Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
The concept and environment
C# 14's field contextual keyword lets a property accessor use compiler-synthesized backing storage while retaining custom validation. The external Request property has an automatic getter and a setter that assigns field only for a nonnegative value. The portable .NET 9 companion writes an explicit private quantity field and equivalent accessors. That companion compares the validation behavior; it does not enable the field feature in the browser compiler. The distinction is useful because property invariants are observable domain behavior, whereas choosing synthesized or explicitly named storage is a separate language and maintenance decision.
Walk through the portable program
A fresh Request begins with quantity two. Assigning negative one evaluates the rejection branch, throws ArgumentOutOfRangeException, and leaves the prior stored value intact. The local observation records that the expected exception was seen. A later valid assignment stores five. The baseline prints initial:2, rejected:True, and current:5. It does not print the intermediate post-rejection value, so that is a useful independent practice observation. The successful final assignment must not be mistaken for evidence that rejection alone preserved state; both obligations deserve their own checks.
Compile the genuine external feature
The real external setter uses field = ... instead of declaring a private field manually. The compiler provides storage for the property and the automatic getter reads it. The initializer establishes the valid initial value two; do not assume a property initializer executes the custom setter as though it were an ordinary later assignment. The supplied net10.0 project selects C# 14.0 explicitly and uses no preview switch. Compiling that project checks the actual accessor syntax, while running it verifies the bounded validation outcomes against the same independently expected baseline.
Pitfalls and limits
Use the contextual keyword only where its accessor meaning is intended. When migrating an existing type with a member actually named field, inspect binding and use explicit qualification or escaping as appropriate rather than assuming every old identifier keeps its previous meaning. Also avoid recursively assigning the property from its own setter when the intention is to update backing storage. The new keyword does not make invalid initializers valid, add thread safety, or perform domain validation automatically. The programmer still owns the range rule, exception contract, initialization choices, and all other mutation paths into the model.
Practice one: isolate a contract
First print Quantity immediately after catching the rejected assignment and predict two. Then assign zero and predict acceptance, since the specified boundary is nonnegative rather than strictly positive. Hint: choose observations that distinguish the lower comparison and state-preservation rule. Keep the original expected exception type instead of accepting any exception as validation success. Run the portable and external programs independently with the same cases, and record where their syntax differs without claiming that identical output demonstrates identical compiler lowering or object layout.
Practice two: challenge an assumption
For a second exercise, change the requirement to accept only one through five. Add cases at zero, one, five, and six, and check that each rejection preserves the last valid quantity. Hint: arrange a valid prior value before testing failure, so a default zero cannot mask an accidental reset. Decide how construction establishes that narrower invariant as well as later setters. If a caller needs to report invalid input without exceptions, design a separate result-returning boundary rather than silently discarding bad assignments inside the property.
Review the resulting contract
A property review should list its initial state, accepted values, rejected outcomes, and state changes on failure before discussing storage syntax. The field feature removes some backing-field boilerplate while leaving those responsibilities intact. This lesson deliberately compares a complete explicit implementation with a complete external C# 14 implementation so the feature remains verifiable. Keep environment requirements visible, verify the actual setter branches, and avoid treating the presence of a concise accessor as evidence of immutability or correctness. The useful guarantee comes from the implemented contract and its independent observations.
C# 14 field-backed property validation lab
Set up the SDK project
- Save every listed file at its exact relative path in a fresh directory.
- Install a .NET 10 SDK supporting stable C# 14; check dotnet --version.
- Run dotnet run --project FeatureLab.csproj. Keep this external code separate from the .NET 9 playground.
Download complete SDK project ↗
Project files
Program.cs
Download file ↗var request = new Request();
Console.WriteLine($"initial:{request.Quantity}");
bool rejected = false;
try { request.Quantity = -1; } catch (ArgumentOutOfRangeException) { rejected = true; }
Console.WriteLine($"rejected:{rejected}");
request.Quantity = 5;
Console.WriteLine($"current:{request.Quantity}");
Request.cs
Download file ↗public sealed class Request
{
public int Quantity
{
get;
set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
} = 2;
}
FeatureLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Expected result
Expected console output, in this order: initial:2 rejected:True current:5 The portable companion compares the same small rule using older syntax; it does not compile or emulate the new feature.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
var request = new Request();
Console.WriteLine($"initial:{request.Quantity}");
bool rejected = false;
try { request.Quantity = -1; } catch (ArgumentOutOfRangeException) { rejected = true; }
Console.WriteLine($"rejected:{rejected}");
request.Quantity = 5;
Console.WriteLine($"current:{request.Quantity}");
sealed class Request
{
private int quantity = 2;
public int Quantity
{
get => quantity;
set => quantity = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
}
}
C# practice output
initial:2 rejected:True current:5
Observe the value immediately after rejection; it remains two. Compile field in the external project, not this explicit-storage companion.