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 allows assignment through null-conditional member access on a reference receiver. A central behavior is that the right side is evaluated only when that receiver is nonnull. The real syntax appears in the external .NET 10 source as assignment to customer followed by a null-conditional access. The portable companion uses explicit if guards to compare the same effect policy. It does not parse or emulate the new syntax. Tracking the local Build call count makes conditional evaluation observable without contacting any service or relying on hidden side effects.
Walk through the portable program
The first customer variable is null, so its guarded assignment does not call Build and the first line is calls:0. A Customer is then constructed with status new. The second assignment reaches Build, increments calls to one, and stores ready. The remaining lines are status:ready and calls:1. The final status alone could not show whether an earlier unnecessary Build happened, which is why the counter is also observed. Every result follows directly from the receiver state and the placement of the expression inside the guard.
Compile the genuine external feature
The external program replaces each explicit guard with actual null-conditional assignment and retains the same independently predicted results. Its project targets net10.0 with stable C# 14.0. This compiler feature can express the intent compactly, including supported element and compound assignments, but it does not turn every expression into a writable variable. In particular, the language does not permit simply applying increment or decrement through this access form. Use the actual project to explore permitted syntax; a helper method producing similar values cannot establish that a compiler accepts a new assignment shape.
Pitfalls and limits
Moving Build into a local computed before the guard changes the effect policy because it runs even when the receiver is absent. Also be careful when translating a complex receiver expression manually: evaluating a property or factory twice can differ from checking and using one captured result. This sample uses a stable local and therefore avoids that extra issue. Null-conditional access only addresses a null receiver; it does not suppress an exception thrown by a property setter, fix an invalid index on a present collection, or make a concurrent update atomic.
Practice one: isolate a contract
First keep customer null for both assignments and predict two printed call counts of zero with no Build invocation. Adjust any final status observation to use safe reading so the exercise does not introduce an unrelated null dereference. Hint: distinguish the assignment's skipped effect from whatever diagnostic expression you add afterward. Then construct a customer before the first assignment and predict one call at the first observation and two at the final observation. Use these cases to check the causal rule rather than merely comparing the last stored text.
Practice two: challenge an assumption
For another exercise, create a receiver-producing function that increments a separate lookup counter. Capture its result once in the portable version before checking it, and compare with one null-conditional assignment in the external version. Hint: receiver evaluation and right-side evaluation are different effects and deserve separate counters. Test both an absent and present receiver. Finally make the setter deliberately reject a value and observe that a nonnull receiver still exposes that exception; conditional access is not a general exception-handling or validation mechanism.
Review the resulting contract
Review conditional assignment by asking whether the receiver can be absent, when the right side should run, and whether evaluating the receiver itself has effects. Verify both null and nonnull cases and retain a concrete boundary observation such as the call counter. The external project establishes the real C# 14 syntax, while the portable guard establishes the small policy in the present engine. Keep that division explicit. Concise assignment is useful when it accurately expresses an optional update, but it should not hide expensive work or imply guarantees beyond null-conditioned execution.
C# 14 null-conditional assignment 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 ↗Customer? customer = null;
int calls = 0;
string Build() { calls++; return "ready"; }
customer?.Status = Build();
Console.WriteLine($"calls:{calls}");
customer = new Customer();
customer?.Status = Build();
Console.WriteLine($"status:{customer.Status}");
Console.WriteLine($"calls:{calls}");
Customer.cs
Download file ↗public sealed class Customer { public string Status { get; set; } = "new"; }
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: calls:0 status:ready calls:1 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;
Customer? customer = null;
int calls = 0;
string Build() { calls++; return "ready"; }
if (customer is not null) customer.Status = Build();
Console.WriteLine($"calls:{calls}");
customer = new Customer();
if (customer is not null) customer.Status = Build();
Console.WriteLine($"status:{customer.Status}");
Console.WriteLine($"calls:{calls}");
sealed class Customer { public string Status { get; set; } = "new"; }
C# practice output
calls:0 status:ready calls:1
Keep Build inside the guard. Computing it beforehand changes side effects for an absent receiver.