Keep it close

A little clarity.
Right when you need it.

170 reference entries. Real explanations, small examples, and a way to keep moving.

170 results

Text

Ordinal text comparison

Use explicit equality rules for identifiers.

Read explanation & example

Identifier comparison should follow a documented rule rather than the device’s display culture. StringComparer.Ordinal compares the underlying string values deterministically, while OrdinalIgnoreCase also ignores case using ordinal rules. Human-language sorting is a different task and may need culture-aware comparison. Choose the comparer at collection construction so membership and lookup agree. The sample deliberately treats two spellings as one identifier; a password, opaque token, or case-sensitive file path may require the opposite policy.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Rune" };
Console.WriteLine(ids.Contains("rune"));
Microsoft documentation ↗
Text

Strings are immutable

A string operation returns a new value.

Read explanation & example

A string’s content cannot be edited in place. Replace returns another string, and a variable changes only when you assign the returned value. Sharing a string reference is safe from later character mutation because no caller can rewrite that instance’s characters. This does not make the variable itself immutable; it can still be assigned another string. Distinguish the lifetime of a value from the lifetime of a variable when tracing data through a program.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var original = "rune";
var changed = original.Replace("r", "d");
Console.WriteLine(original);
Console.WriteLine(changed);
Microsoft documentation ↗
Text

StringBuilder for repeated assembly

Accumulate many text fragments in a mutable builder.

Read explanation & example

Repeatedly appending to an immutable string may create intermediate strings and copy earlier content. StringBuilder offers a mutable buffer for incremental assembly, then ToString produces the final string. It is useful for reports or loops with many fragments, but simple interpolation is usually clearer for a few known values. Do not assume a builder is automatically faster for every expression. Measure meaningful workloads and keep the builder local unless shared access has an explicit synchronization policy.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var builder = new StringBuilder();
for (var i = 1; i <= 3; i++) builder.Append(i).Append(';');
Console.WriteLine(builder.ToString());
Microsoft documentation ↗
Text

Unicode scalar values

A char does not always represent a complete displayed character.

Read explanation & example

A .NET char is a UTF-16 code unit. Some Unicode scalar values use two code units, so string.Length can differ from the number of scalar values. EnumerateRunes reads Unicode scalar values rather than individual surrogate halves. A displayed grapheme can still contain several scalar values, such as a letter plus a combining mark or a joined emoji sequence. Choose the unit your feature needs before implementing a character limit, cursor movement, truncation, or a text counter.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var text = "A😀";
Console.WriteLine(text.Length);
Console.WriteLine(text.EnumerateRunes().Count());
Microsoft documentation ↗
Text

Unicode normalization

Visually similar text can have different underlying sequences.

Read explanation & example

An accented letter can be represented as one scalar value or as a base letter followed by a combining mark. Normalization converts supported equivalent representations into a chosen normalization form. This can help when an application defines identifiers or search keys with canonical equivalence. It does not make every visually similar symbol equal, translate text, or solve spoofing. Preserve original user-facing text when appropriate and document how normalized keys are produced before storing or comparing them. Normalization depends on the host’s globalization support. SharpRune’s invariant browser profile is not a suitable proof of canonical Unicode matching; verify the intended production host before relying on this operation.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var combined = "é";
var separate = "é";
Console.WriteLine(combined == separate);
Console.WriteLine(combined.Normalize() == separate.Normalize());
Microsoft documentation ↗
Input

TryParse and invalid text

Expected conversion failure can be a normal result.

Read explanation & example

TryParse returns a success flag and writes the converted value through an out parameter. A false result means the caller must not treat that output as a successfully parsed input, even when its default value appears plausible. Parsing establishes representability, not a domain rule such as a quantity being positive or an age being reasonable. Apply those rules afterwards and distinguish missing, malformed, and out-of-range input when the user needs different correction messages.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var ok = int.TryParse("not a number", out var value);
Console.WriteLine(ok);
Console.WriteLine(value);
Microsoft documentation ↗
Numbers

Checked integer arithmetic

Detect overflow when a wrapped value would violate the result contract.

Read explanation & example

Fixed-width integers have finite ranges. In a checked context, an overflowing integer operation throws OverflowException instead of silently producing a wrapped result. Whether to reject, saturate, use a wider type, or use arbitrary precision depends on the domain. Converting an operand to long before multiplication changes where the arithmetic occurs; converting an already overflowed int result afterwards is too late. Test boundary values when totals or user-controlled sizes can grow beyond ordinary examples.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var largest = int.MaxValue;
try { Console.WriteLine(checked(largest + 1)); }
catch (OverflowException) { Console.WriteLine("overflow"); }
Microsoft documentation ↗
Numbers

Rounding is a policy

State the rounding mode and the point where it applies.

Read explanation & example

Rounding changes a value according to a chosen number of digits and midpoint rule. Decimal.Round can specify MidpointRounding explicitly, which prevents a reader from guessing how a halfway case behaves. Financial rules may require rounding per item, per tax category, or only after aggregation; those choices can produce different totals. Decimal arithmetic does not select that policy for you. Keep numeric rounding separate from formatting when later operations still need an actual rounded amount.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine(decimal.Round(2.5m, 0, MidpointRounding.ToEven));
Console.WriteLine(decimal.Round(2.5m, 0, MidpointRounding.AwayFromZero));
Microsoft documentation ↗
Numbers

Floating-point comparisons

Compare approximate measurements with a meaningful tolerance.

Read explanation & example

Binary floating-point types approximate many decimal fractions. A calculation can therefore produce a value close to an expected result without being exactly equal to its decimal-looking literal. A tolerance can make sense for measurements, but it must fit the scale and error budget of the calculation. A fixed epsilon is not a universal solution for values spanning very different magnitudes. Use decimal for appropriate base-ten financial calculations and preserve the units associated with every numeric quantity.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var actual = 0.1 + 0.2;
Console.WriteLine(actual == 0.3);
Console.WriteLine(Math.Abs(actual - 0.3) < 1e-12);
Microsoft documentation ↗
Numbers

Integer division truncates

Choose operand types before dividing.

Read explanation & example

Dividing two integers produces an integer result with the fractional portion truncated toward zero. Assigning that result into a decimal afterwards cannot recover the discarded fraction. Convert an operand before division or use an appropriate literal suffix so the operation itself uses the intended numeric type. Also handle a zero denominator explicitly. The right type depends on whether the result represents a count, a ratio, money, or an approximate physical measurement; a wider variable declaration alone does not define the arithmetic.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine(5 / 2);
Console.WriteLine((5m / 2).ToString(CultureInfo.InvariantCulture));
Microsoft documentation ↗
Types

Null and empty are different

Missing data and present-but-empty data can carry different meanings.

Read explanation & example

Null indicates the absence of a referenced value, while an empty string is an actual string with no characters. A domain may intentionally distinguish an omitted field from a field explicitly cleared by a user. IsNullOrWhiteSpace combines several checks when all those cases should be rejected together. Do not normalize every empty value into null merely for convenience if that erases a meaningful update instruction. Define the boundary behavior before mapping external documents into stored application state.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

string? missing = null;
Console.WriteLine(missing is null);
Console.WriteLine("".Length);
Console.WriteLine(string.IsNullOrWhiteSpace("  "));
Microsoft documentation ↗
Types

Null-conditional access

Skip one access when its receiver is absent.

Read explanation & example

The null-conditional operator evaluates a member access only when the receiver is not null. It returns a nullable result when the ordinary member result is a value type, allowing a later coalescing expression to choose a display fallback. It does not validate the entire object graph or catch exceptions thrown inside a property getter. Use it to express optional data access, and keep required data requirements explicit so a fallback does not conceal a broken invariant.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

string? name = null;
Console.WriteLine(name?.Length ?? -1);
Microsoft documentation ↗
Types

Validate enum inputs

An enum variable can contain an undefined numeric value.

Read explanation & example

An enum gives names to selected numeric values, but a cast from an integer can create a value with no declared member. Enum.IsDefined can check ordinary enum inputs when only declared members are allowed. Flags enums need different validation because meaningful combinations may not be individually declared. Do not assume a deserialized or cast enum is valid just because it has the correct static type. Validate at the boundary and decide how future unknown values should affect compatibility.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var state = (State)9;
Console.WriteLine(Enum.IsDefined(state));
enum State { Pending = 0, Complete = 1 }
Microsoft documentation ↗
Types

Shallow copies share nested objects

Copying a container does not recursively clone its elements.

Read explanation & example

Creating another array or using a record with expression copies the values held by that container. When an element or property is a reference, the copied value is another reference to the same object. Mutating that object remains visible through both owners. If independent snapshots are required, choose immutable nested values or implement an explicit deep-copy policy. Deep copying arbitrary graphs is not a trivial default because identity, cycles, resources, and intentional sharing all need decisions.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var first = new[] { new Box { Count = 1 } };
var second = first.ToArray();
second[0].Count = 8;
Console.WriteLine(first[0].Count);
class Box { public int Count { get; set; } }
Microsoft documentation ↗
Collections

Read-only access is not immutable storage

A restricted interface can still observe changes made by its owner.

Read explanation & example

IReadOnlyList<T> prevents mutation through that interface, but it does not guarantee the underlying collection never changes. A caller holding the original List<T> can still add or remove elements, and the read-only view will observe those changes. This can be useful when ownership is clear. If a result must remain stable after a method returns, materialize a copy or choose an immutable representation. Also consider whether the elements themselves expose mutable state, since a fixed collection can still contain changing objects.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var owner = new List<int> { 1 };
IReadOnlyList<int> view = owner;
owner.Add(2);
Console.WriteLine(view.Count);
Microsoft documentation ↗
Collections

Capacity and count

Allocated storage and logical membership answer different questions.

Read explanation & example

A list’s Count reports how many elements are present, while Capacity reports how much internal storage is currently available before another growth allocation may be needed. Reserving capacity can reduce repeated allocations when an approximate size is known, but it does not create logical elements. Accessing an index still requires that it be below Count. Avoid reserving huge capacities from untrusted input without a limit; an optimization hint can become a memory demand before any useful work starts.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var values = new List<int>(10);
Console.WriteLine(values.Count);
values.Add(7);
Console.WriteLine(values.Count);
Microsoft documentation ↗
Collections

Dictionary missing keys

Use TryGetValue when absence belongs to normal control flow.

Read explanation & example

The dictionary indexer throws for a missing key. TryGetValue instead reports whether a mapping exists and supplies its value when found, making normal absence explicit. A default value such as zero can also be a real stored value, so the success flag matters. Choose whether missing data means a default, a validation error, or an incomplete report at the consuming boundary. A dictionary lookup does not establish that a key was valid in the wider domain or that the stored object remains current.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var counts = new Dictionary<string, int> { ["A"] = 0 };
Console.WriteLine(counts.TryGetValue("A", out var a));
Console.WriteLine(counts.TryGetValue("B", out var b));
Microsoft documentation ↗
Collections

Mutation during enumeration

Avoid changing a collection while a loop is reading its enumerator.

Read explanation & example

Many mutable collection enumerators reject structural changes made during enumeration. This catches code whose traversal assumptions were invalidated by an Add or Remove. When removing selected items from a List<T>, use RemoveAll with a predicate or iterate over a deliberate snapshot. A snapshot costs memory and changes when membership is observed, so choose it for a reason. Changing fields on objects inside a collection is a different operation and still needs an ownership policy even when the enumerator allows it.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var values = new List<int> { 1, 2, 3, 4 };
values.RemoveAll(x => x % 2 == 0);
Console.WriteLine(string.Join(",", values));
Microsoft documentation ↗
LINQ

Empty-sequence contracts

Pick an operator whose empty-input behavior matches the requirement.

Read explanation & example

Sequence operations do not share one universal empty-input result. Any returns false, All returns true, Count returns zero, and First throws unless a matching element exists. FirstOrDefault can return a default value that is indistinguishable from a legitimate element unless the type or result contract preserves absence. Decide the meaning of an empty collection before choosing a convenient operator. For a required order, absence may be an error; for an optional reading, it may belong in a nullable result.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine(Array.Empty<int>().Any());
Console.WriteLine(Array.Empty<int>().All(x => x > 0));
Console.WriteLine(Array.Empty<int>().FirstOrDefault());
Microsoft documentation ↗
LINQ

Single versus First

Use uniqueness checks when the rule requires exactly one match.

Read explanation & example

First asks for the earliest matching element and does not establish that another match is absent. Single requires exactly one element and throws when the sequence has zero or multiple elements. SingleOrDefault allows no element but still rejects multiple matches. These operators express different domain assumptions, not stylistic preferences. If duplicate identifiers indicate a data error, selecting the first can conceal it. If many matches are expected and order chooses one intentionally, document that order before using First.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var ids = new[] { 7, 7 };
Console.WriteLine(ids.First());
try { Console.WriteLine(ids.Single()); }
catch (InvalidOperationException) { Console.WriteLine("not unique"); }
Microsoft documentation ↗
LINQ

Side effects in queries

Enumeration can repeat a projection’s observable work.

Read explanation & example

A deferred query can run its selector again on every enumeration. Logging, mutation, or external writes inside that selector may therefore happen several times even if the query variable was assigned once. Keep query transformations pure when possible, and put deliberate side effects in a clearly owned loop. Materialization can establish one evaluation boundary but is not a substitute for understanding ownership. A database provider may also translate or reject expressions, so an in-memory side effect is not a portable query contract.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var calls = 0;
var query = new[] { 1, 2 }.Select(x => { calls++; return x * 2; });
query.ToArray(); query.ToArray();
Console.WriteLine(calls);
Microsoft documentation ↗
LINQ

Lookup supports repeated group access

Prepare a multi-value index when keys can have several members.

Read explanation & example

ToLookup materializes a mapping from each key to a sequence of matching elements. Unlike ToDictionary with one element per key, it naturally represents repeated keys. Reading a missing key produces an empty sequence, which can make report code straightforward when absence means no members. The lookup fixes group membership when it is created, although referenced objects can remain mutable. Use it when repeated access by group justifies preparation; a single pass over a tiny collection may not need an index.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var lookup = new[] { "ant", "ape", "bee" }.ToLookup(x => x[0]);
Console.WriteLine(string.Join(",", lookup['a']));
Console.WriteLine(lookup['z'].Count());
Microsoft documentation ↗
LINQ

Distinct needs equality rules

Deduplicate according to the data’s actual identity.

Read explanation & example

Distinct removes repeated elements according to its equality comparer. For strings, the chosen comparer determines whether differently cased spellings represent the same value. Deduplication cannot infer business identity from a display label: two people can have the same name, while two spellings can refer to one identifier. Make identity explicit before discarding data. If the output order is part of a public contract, apply a deliberate ordering rather than depending on implementation details of a set-like operation.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var values = new[] { "Rune", "rune", "Code" };
Console.WriteLine(string.Join(",", values.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.Ordinal)));
Microsoft documentation ↗
Async

Cancellation source ownership

The operation owner controls the cancellation lifetime.

Read explanation & example

A CancellationTokenSource owns the ability to request cancellation, while a token exposes observation to dependent work. Passing tokens downward keeps control with the caller and avoids giving helpers unnecessary authority over sibling operations. Disposing a source releases its resources but does not mean cancellation was requested. Cancellation remains cooperative: a method must check the token or pass it to a cancellable dependency. Treat expected user cancellation separately from a failure message, and keep cleanup tied to the operation’s owned resources.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using var source = new CancellationTokenSource();
var token = source.Token;
Console.WriteLine(token.IsCancellationRequested);
source.Cancel();
Console.WriteLine(token.IsCancellationRequested);
Microsoft documentation ↗
Async

Return Task from async operations

Let the caller observe completion and failure.

Read explanation & example

An async method returning Task gives callers a handle they can await and compose. Async void is mainly intended for event handlers whose signature cannot return a task; ordinary callers cannot await its completion or observe errors through a returned task. Keep event handlers thin by awaiting a Task-returning helper. Fire-and-forget work needs an explicit owner, error channel, and shutdown policy rather than simply omitting await. A method’s lifetime should remain visible to the code responsible for the user action.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

await SaveAsync();
Console.WriteLine("finished");
async Task SaveAsync() { await Task.Delay(1); }
Microsoft documentation ↗
Async

WhenAll and multiple failures

Observe the combined task when more than one operation can fail.

Read explanation & example

WhenAll completes after all supplied tasks complete. If several fail, its task stores their failures; awaiting it throws an exception into the caller’s normal control flow. When diagnostics need every failure, inspect the completed combined task’s Exception and its inner exceptions rather than assuming one caught message describes every operation. Cancellation has separate completion semantics, and WhenAll does not automatically cancel sibling work. Decide whether a failed operation should request cancellation of its peers through an owner-controlled source.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var combined = Task.WhenAll(Task.FromException<int>(new InvalidOperationException("A")), Task.FromException<int>(new ArgumentException("B")));
try { await combined; } catch { Console.WriteLine(combined.Exception!.InnerExceptions.Count); }
Microsoft documentation ↗
Async

Async is not parallelism

Waiting asynchronously does not promise another CPU thread.

Read explanation & example

Async and await describe how a method waits for completion without blocking its logical flow. They do not automatically move CPU-intensive work to another thread. A method can run substantial synchronous code before its first incomplete await, so an async name alone says little about responsiveness. Browser-hosted .NET also has environment-specific threading capabilities. Keep computational workloads bounded, use cancellation checks where meaningful, and verify the runtime’s actual threading support before introducing Task.Run as a supposed universal fix.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine("before");
await Task.CompletedTask;
Console.WriteLine("after");
Microsoft documentation ↗
Async

Avoid synchronous task waits

Keep asynchronous completion visible through the call chain.

Read explanation & example

Task.Result and Wait synchronously block while waiting for completion. In environments with synchronization contexts, blocking can also interact badly with continuations that need the blocked context. Await expresses the dependency without forcing that blocking shape. An asynchronous API should usually remain asynchronous through its callers until it reaches a framework entry point. Wrapping a blocking call in another method does not remove the lifetime or responsiveness problem, and a completed-task example is not evidence that blocking will be safe under real latency.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var value = await Task.FromResult(4);
Console.WriteLine(value);
Microsoft documentation ↗
Async

Timeout and cancellation differ

Stopping the wait does not prove the underlying operation stopped.

Read explanation & example

A timeout limits how long a caller is willing to wait. Cancellation requests that an operation stop cooperatively. APIs can combine these ideas, but they remain distinct: work may continue after one observer times out if it has not received or honored a cancellation request. Keep a handle to outstanding work, propagate tokens to dependencies, and observe its eventual completion or failure. In a browser playground, terminating a worker is a separate host action with different effects from an application token check.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using var source = new CancellationTokenSource();
source.Cancel();
try { await Task.Delay(1000, source.Token); }
catch (OperationCanceledException) { Console.WriteLine("request observed"); }
Microsoft documentation ↗
Dates

An instant with an offset

Use a timestamp representation that preserves its timeline meaning.

Read explanation & example

DateTimeOffset combines a date and time with an offset from UTC, allowing it to identify an instant. Two displayed clock times can represent the same instant when their offsets differ. An offset is not a complete time-zone rule set, so recurring future appointments may also need a named time zone and daylight-saving policy. Keep a date-only birthday separate from an event timestamp. For interchange, use a documented invariant format and preserve enough information for the comparisons the domain actually requires.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var first = DateTimeOffset.Parse("2026-01-01T12:00:00+02:00", CultureInfo.InvariantCulture);
Console.WriteLine(first.UtcDateTime.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture));
Microsoft documentation ↗
Dates

Durations and clock times

A duration measures elapsed quantity rather than a calendar appointment.

Read explanation & example

TimeSpan represents a duration, such as ninety seconds or two hours. It does not identify a date or time zone. TotalMinutes expresses the whole duration as a fractional number of minutes, while Minutes is only the minutes component after larger units are separated. Confusing the two can corrupt progress displays and timeout calculations. Calendar periods such as “one month” need additional rules because months have different lengths. Choose units explicitly when accepting numeric duration settings from users or configuration.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var duration = TimeSpan.FromMinutes(90);
Console.WriteLine(duration.Hours);
Console.WriteLine(duration.TotalMinutes);
Microsoft documentation ↗
JSON

Unknown JSON properties

Decide how readers handle fields they do not recognize.

Read explanation & example

A format evolves when writers add, rename, or remove fields. A reader’s policy for unknown properties determines whether old software can tolerate newer documents or must reject them. Ignoring unknown fields can support additive evolution, but it can also hide a misspelled property that leaves a required value unset. Separate compatibility policy from required-field and domain validation. Store schema versions when meaningful, test old fixtures, and document which changes are compatible rather than treating serializer success as proof of a valid migration.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var item = JsonSerializer.Deserialize<Item>("""{"Count":2,"FutureField":true}""");
Console.WriteLine(item!.Count);
record Item(int Count);
Microsoft documentation ↗
JSON

JSON numeric types

A JSON string containing digits is not automatically a numeric value.

Read explanation & example

JSON distinguishes a number token from a quoted string token. A typed serializer ordinarily enforces that distinction unless options or attributes intentionally allow another representation. Accepting both can help with a legacy contract, but it also broadens the format and can conceal inconsistent writers. Decide the representation at the API boundary, preserve decimal values as numbers when consumers need arithmetic, and never treat successful conversion as permission to ignore range, currency, precision, or required-field validation.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using var doc = JsonDocument.Parse("""{"number":3,"text":"3"}""");
Console.WriteLine(doc.RootElement.GetProperty("number").ValueKind);
Console.WriteLine(doc.RootElement.GetProperty("text").ValueKind);
Microsoft documentation ↗
JSON

JsonDocument lifetime

A document owns the memory used by its element views.

Read explanation & example

JsonElement commonly acts as a view into the owning JsonDocument. Disposing the document ends access to that backing data, so returning an element from a helper that has already disposed its document is unsafe unless the element is cloned. Clone creates an element that can outlive the original document. Prefer typed values when only a few fields are needed beyond the parse scope. Keep document lifetime explicit and avoid retaining an entire large document merely to preserve one small property.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

JsonElement saved;
using (var document = JsonDocument.Parse("""{"count":3}""")) saved = document.RootElement.Clone();
Console.WriteLine(saved.GetProperty("count").GetInt32());
Microsoft documentation ↗
Runtime

Deterministic disposal

Use ownership to decide who releases a disposable resource.

Read explanation & example

A using declaration calls Dispose when control leaves its scope, including when an exception escapes. This deterministic cleanup is different from eventual garbage collection of managed memory. The code that creates and owns a resource should normally define its disposal boundary, while a helper borrowing it should follow its documented contract. Disposing an object does not necessarily erase all managed memory or guarantee secure deletion. Prefer small scopes and make ownership visible instead of depending on a finalizer to release an external resource promptly.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using var stream = new System.IO.MemoryStream();
stream.WriteByte(7);
Console.WriteLine(stream.Length);
Microsoft documentation ↗
Errors

Exceptions and expected failures

Use the failure channel that matches the operation’s contract.

Read explanation & example

An exception reports a condition that prevents an operation from completing according to its contract. Expected invalid user input can often be represented by a Try method or a structured validation result instead. Catch only where code can take a meaningful action, preserve the original exception when rethrowing, and avoid returning plausible success data after an unexpected failure. Logging and displaying an error are separate decisions: raw messages can contain input details and may not be stable text for a user interface.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

if (!int.TryParse("oops", out var quantity)) Console.WriteLine("Enter an integer");
Microsoft documentation ↗
Design

Separate calculation and effects

Return a value from reusable logic and present it at the edge.

Read explanation & example

A calculation that depends only on explicit inputs is easier to reuse and verify than one that also prints, saves files, and changes global state. The caller can format its result for a console, a web response, or a lesson output without changing the calculation. Purity is a design tool rather than a claim that applications need no effects. Keep effects at clear boundaries, pass required data explicitly, and test observable outcomes that express the contract rather than the private sequence of operations.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine(Total(new[] { 2m, 3m }));
decimal Total(IEnumerable<decimal> values) => values.Sum();
Microsoft documentation ↗
Runtime

Browser runtime boundaries

A .NET API’s presence does not guarantee every host capability exists.

Read explanation & example

WebAssembly runs inside a host environment whose capabilities differ from an operating-system process. A browser playground may compile managed C# successfully while still rejecting native libraries, process creation, raw sockets, or other unsupported operations at runtime. Treat package compatibility as a combination of target framework, managed dependencies, host APIs, and actual verification. Local virtual files are not automatically files on the user’s disk. Build examples around the documented profile and verify behavior in the browser rather than inferring it from desktop execution alone.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine("Managed calculation");
Console.WriteLine(new[] { 2, 3 }.Sum());
Microsoft documentation ↗
Design

Test boundaries, not implementation steps

Use examples that can distinguish correct and incorrect behavior.

Read explanation & example

A useful check states an input and its required observable result. Boundary cases such as empty input, one item, duplicates, negative values, and the exact threshold often distinguish a correct implementation from a plausible broken one. Avoid tests that merely repeat the algorithm in another form or inspect its private loop choices. A different implementation should be able to satisfy the same contract. When a result depends on culture, time, randomness, or external state, make that dependency explicit so the check can remain deterministic.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine(IsValid(1));
Console.WriteLine(IsValid(0));
bool IsValid(int quantity) => quantity > 0;
Microsoft documentation ↗
Algorithms

Reason about growth

Count the work that grows with the input rather than just source lines.

Read explanation & example

A compact expression can perform much more work than a longer loop. Searching an entire collection inside a loop over that collection can grow quadratically, while building a set once and performing membership checks can avoid repeated scans. Big-O describes growth rather than a complete runtime prediction: constants, allocations, data size, and the execution environment still matter. Start with a correct clear algorithm, identify realistic input bounds, and measure representative workloads before adding complexity to optimize an unobserved bottleneck.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var allowed = new HashSet<int>(new[] { 2, 4, 6 });
Console.WriteLine(new[] { 1, 2, 3, 4 }.Count(allowed.Contains));
Microsoft documentation ↗
Types

Required initialization and content validation

Supplying a member does not prove that its value satisfies the domain.

Read explanation & example

The required modifier establishes a construction requirement for ordinary C# callers: they must supply the declared member. It does not make an empty string meaningful, and nullable warnings are a different mechanism from runtime rejection. In the example, an empty Name is still an assigned Name, so object construction succeeds while a nonblank-name check can still fail. Use required for presence at construction and a separate validation boundary for allowed content. Adding init restricts later assignments through that property but does not recursively freeze nested mutable objects. A model's invariants need an explicit ownership and validation design. To explore the compiler boundary, temporarily remove Name from the valid initializer and run Check. Restore it before executing the examples. Then test whitespace, an accepted name, and any length restrictions your application actually needs. Distinguishing missing initialization from invalid supplied content gives callers more precise failures than treating every incomplete object as the same problem.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var user = new Profile { Name = "Ada" };
Console.WriteLine(user.Name);
Console.WriteLine(string.IsNullOrWhiteSpace(new Profile { Name = "" }.Name));
class Profile { public required string Name { get; init; } }
Microsoft documentation ↗
Types

Covariance follows the operations exposed

A producer view can widen its element type without becoming writable.

Read explanation & example

IReadOnlyList<Dog> can be viewed as IReadOnlyList<Animal> because reading a Dog as an Animal is safe. That relationship does not imply that List<Dog> can become a writable List<Animal>: allowing an arbitrary Animal to be inserted would violate the original list's element contract. The conversion creates another view, not a copy. Another owner of the underlying mutable list can still change its contents, so covariance is not a snapshot or immutability promise. It answers which type relationships are safe for the operations exposed by an interface. When designing a consumer that only reads, accept a suitably narrow interface such as IEnumerable<Animal> instead of requiring a writable list. Test the distinction by adding a dog through a retained mutable list reference and observing the read-only view's updated count. Document copying separately when callers need independent storage rather than merely fewer available operations.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

IReadOnlyList<Dog> dogs = new List<Dog> { new Dog("Pip") };
IReadOnlyList<Animal> animals = dogs;
Console.WriteLine(animals[0].Name);
class Animal { public string Name { get; } public Animal(string name) { Name = name; } }
class Dog : Animal { public Dog(string name) : base(name) { } }
Microsoft documentation ↗
Types

Extension methods carry static policy

Dot syntax does not add state or override a type’s virtual behavior.

Read explanation & example

An extension method is a static method available through instance-style call syntax when its namespace is in scope. NormalizeId receives a string, transforms it, and returns a string. It does not modify the original string or add hidden per-instance storage to System.String. The convenient syntax can conceal a policy decision. Trimming and invariant lowercasing may be appropriate for one identifier but inappropriate for an opaque token or case-sensitive value. Name the operation according to its real contract and specify how null and empty inputs are handled. Do not distribute a broad collection of vaguely named extensions across every namespace. A focused set of operations is easier to discover and less likely to create confusing overload choices. In the sample, try whitespace-only input and decide whether normalization should return an empty value or a separate validator should reject it. Transformation and acceptance rules deserve distinct tests.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

Console.WriteLine("  rune  ".NormalizeId());
Console.WriteLine("RUNE".NormalizeId());
static class IdExtensions {
    public static string NormalizeId(this string value) {
        return value.Trim().ToLowerInvariant();
    }
}
Microsoft documentation ↗
Functions

Closures and per-iteration snapshots

A delayed delegate can observe later changes to a captured variable.

Read explanation & example

A closure retains access to an outer variable. The first delegate in the example therefore observes adjustment changing from two to five; it did not freeze a value at the moment the lambda was written. This behavior is intentional when callbacks should follow live state, but surprising when each operation needs its own fixed input. The for-loop example creates a snapshot local inside each iteration. Each stored delegate captures a different local and later returns the intended zero, one, or two. Capturing the for variable directly would give the delegates a shared variable whose value changes as the loop advances. Test callbacks after the constructing loop finishes, not only while each iteration is running. Also consider lifetime: a retained delegate can keep referenced objects reachable. Capture the smallest state the callback needs, and use a static lambda when capture should be forbidden by construction. Timing and ownership are part of a callback's contract.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int adjustment = 2;
Func<int, int> add = value => value + adjustment;
Console.WriteLine(add(10));
adjustment = 5;
Console.WriteLine(add(10));
var actions = new List<Func<int>>();
for (int i = 0; i < 3; i++) {
    int snapshot = i;
    actions.Add(() => snapshot);
}
Console.WriteLine(string.Join(",", actions.Select(action => action())));
Microsoft documentation ↗
LINQ

Chunk remainders are ordinary batches

The last chunk can be shorter without losing any source elements.

Read explanation & example

Chunk(size) divides an enumeration into arrays containing at most size elements. A five-element source with size two produces lengths two, two, and one. Consumers should iterate each chunk's actual length rather than assume every batch is full or discard the remainder. Chunking describes grouping, not concurrency. It neither starts tasks nor limits how many asynchronous operations run at once. A consumer can process batches sequentially or use a separate execution policy, but those choices should remain explicit. For verification, flatten the produced chunks and compare the result with the original ordered source. That catches omitted tails and duplicated elements. Test an empty source, one element, an exact multiple, and a final partial batch. Materializing every chunk may be useful for a small snapshot; a large workflow can process chunks as enumerated instead of retaining the entire batch collection. Validate a configurable size before beginning work.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int[] values = { 1, 2, 3, 4, 5 };
var chunks = values.Chunk(2).ToArray();
Console.WriteLine(string.Join("|", chunks.Select(chunk => string.Join(",", chunk))));
Console.WriteLine(string.Join(",", chunks.Select(chunk => chunk.Length)));
Microsoft documentation ↗
LINQ

Partitioned rank needs a tie definition

Apply the index after sorting within each group.

Read explanation & example

Grouping and ranking are separate operations. The example first partitions scores by team, then sorts each team's members and assigns positions with indexed Select. Placing that Select inside the group resets the index for each team; moving it outside would number the flattened report globally. The displayed rank is a unique position, not dense ranking or competition ranking. Equal scores are ordered by name, so they still receive different numbers. If a product requires shared ranks for tied scores, implement that different definition explicitly instead of merely changing the display label. Capture source positions before filtering when diagnostics need original locations, but introduce leaderboard positions only after the intended ordering. Test multiple groups, a single-member group, ties, and source-order changes. Deterministic tie breakers protect reproducibility, while the ranking policy determines what the numbers actually mean. An index parameter has meaning only relative to its stage in the pipeline.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var scores = new[] { new Score("red", "Ada", 8), new Score("red", "Ben", 10), new Score("blue", "Cy", 9), new Score("blue", "Di", 7) };
var ranked = scores.GroupBy(score => score.Team)
    .OrderBy(group => group.Key, StringComparer.Ordinal)
    .SelectMany(group => group.OrderByDescending(score => score.Points)
        .ThenBy(score => score.Name, StringComparer.Ordinal)
        .Select((score, index) => $"{group.Key}:{score.Name}:{index + 1}"));
foreach (var line in ranked) Console.WriteLine(line);
record Score(string Team, string Name, int Points);
Microsoft documentation ↗
LINQ

Query indices belong to one stage

A later indexed operator numbers the sequence after earlier filters.

Read explanation & example

An indexed Where predicate receives positions from the sequence reaching that operator. After it removes elements, a later indexed Select starts numbering its shorter input again from zero. The original position is not permanently attached to a value unless the pipeline stores it explicitly. The sample's first projection labels the retained values with new positions zero, one, and two. Its second pipeline saves original indices before filtering, preserving zero, two, and four. This distinction matters for import diagnostics that must point to original source lines after comments or blanks have been removed. Use names such as OriginalIndex, SourceLine, and DisplayRank when several positions coexist. An index is not a stable business identifier because inserting an earlier row changes later positions. Test a filtered-out first element and an empty result, and convert zero-based positions to user-facing line numbers at a clear presentation boundary.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int[] values = { 10, 20, 30, 40, 50 };
var selected = values.Where((value, index) => index % 2 == 0);
Console.WriteLine(string.Join(",", selected.Select((value, index) => $"{index}:{value}")));
var original = values.Select((value, index) => new { value, index })
    .Where(row => row.index % 2 == 0);
Console.WriteLine(string.Join(",", original.Select(row => $"{row.index}:{row.value}")));
Microsoft documentation ↗
LINQ

TakeWhile recognizes a prefix

A predicate failure ends the prefix even if later values would match.

Read explanation & example

TakeWhile returns the initial run satisfying a predicate and stops at the first failure. Where examines every element independently. With one, two, negative one, three, the positive prefix contains only one and two, while the positive filter also includes three. SkipWhile skips the initial matching run and then returns the remaining tail, including the first nonmatching element. It is not a filter that continues rejecting later matches. Both while operators depend on an ordered boundary in the source. Use prefix semantics for a consecutive run, delimiter-defined section, or another contract where the first failure really ends the accepted region. Do not use TakeWhile to remove all invalid import rows unless later valid rows are intentionally excluded. Test failure at the first element, no failures, and empty input. Reordering the same values can change a prefix result even when an element-by-element filter keeps the same values.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int[] values = { 1, 2, -1, 3 };
Console.WriteLine(string.Join(",", values.TakeWhile(value => value > 0)));
Console.WriteLine(string.Join(",", values.Where(value => value > 0)));
Console.WriteLine(string.Join(",", values.SkipWhile(value => value > 0)));
Microsoft documentation ↗
Async

Completion authority belongs to one owner

Expose a task to consumers and keep its completion source private.

Read explanation & example

TaskCompletionSource lets an event-producing component complete a task that other code awaits. Its owner can choose a result, a fault, or cancellation. Consumers normally need only the Task property, so they should not receive the authority to manufacture the operation's outcome. A task completes once. TrySetResult returns true for the winning completion signal and false for a later duplicate. The sample preserves seven after a second attempt to replace it with nine. Try-style completion supports explicit handling of duplicate callbacks or competing terminal signals without overwriting history. RunContinuationsAsynchronously reduces direct continuation execution inside a completion setter, but it does not replace lifecycle design. A callback adapter may still need to unregister events and dispose cancellation registrations. Test successful, faulted, canceled, and duplicate-signal paths. Keep completion ownership and cleanup ownership clear so an apparently simple task bridge does not leave event subscriptions or detached work alive.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var completion = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
Console.WriteLine(completion.TrySetResult(7));
Console.WriteLine(completion.TrySetResult(9));
Console.WriteLine(await completion.Task);
Microsoft documentation ↗
Async

Async disposal is part of completion

An await using scope waits for DisposeAsync before it finishes.

Read explanation & example

IAsyncDisposable describes cleanup that can complete asynchronously. await using arranges to await DisposeAsync when the owning scope ends. In the example, released appears before the caller prints finished, even though disposal itself suspends through Task.Yield. The resource belongs to the scope that acquired it. A method borrowing an asynchronous resource should not dispose it without an explicit transfer-of-ownership contract. Cleanup can also fail, so a real workflow must decide how to report a disposal failure alongside a failure from the body. Test ordinary completion, an exception inside the body, and multiple resources whose cleanup occurs in reverse acquisition order. Do not synchronously block on asynchronous disposal merely to fit a synchronous call chain in an environment where that can freeze a user interface. Propagate asynchronous completion to the owner when the resource contract requires it, and observe cleanup before announcing that the owned operation is complete.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

await Use();
Console.WriteLine("finished");
static async Task Use() {
    await using var lease = new AsyncLease();
    Console.WriteLine("working");
}
class AsyncLease : IAsyncDisposable {
    public async ValueTask DisposeAsync() {
        await Task.Yield();
        Console.WriteLine("released");
    }
}
Microsoft documentation ↗
Async

Retries need classification and a budget

Retrying a failure is a policy decision with possible repeated effects.

Read explanation & example

A retry loop should name the transient failure category and cap the total attempts, including the initial call. The sample retries only TransientFailure and allows at most three calls. An unrelated exception propagates immediately, while a final transient failure also propagates once the budget is exhausted. Retried operations can repeat effects. A command may have succeeded even if its response was lost, so repeating it safely can require idempotent semantics or an owner-recognized idempotency key. A transient exception label alone does not prove repetition is harmless. The lesson omits timing to isolate classification and counting. Real policies may also require backoff, jitter, cancellation, and a total workflow deadline. Test immediate success, eventual success, permanent transient failure, and a nontransient failure. Avoid retrying arbitrary exceptions to conceal a persistent programming error; recovery should remain bounded and observable to the component that owns the workflow.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int calls = 0;
int result = await Retry(() => {
    calls++;
    return calls < 3 ? Task.FromException<int>(new TransientFailure()) : Task.FromResult(42);
}, 3);
Console.WriteLine(result);
Console.WriteLine(calls);
static async Task<int> Retry(Func<Task<int>> action, int maxAttempts) {
    if (maxAttempts < 1) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
    for (int attempt = 1; ; attempt++) {
        try { return await action(); }
        catch (TransientFailure) when (attempt < maxAttempts) { }
    }
}
class TransientFailure : Exception { }
Microsoft documentation ↗
Async

A task cache shares an operation lifetime

In-flight sharing also shares faults and cancellation policy.

Read explanation & example

Caching a task lets another caller observe work already in flight rather than start a duplicate operation. The sample returns the same Task reference for two requests and performs one underlying read. This is different from caching only the final value after the operation has completed. The example uses a small dictionary under a coordinated single-owner calling pattern. It is not a thread-safe general cache. Even a concurrent collection's value factory can have invocation semantics that require care, so replacing a container is not sufficient evidence that work starts exactly once under every race. Decide whether faulted and canceled tasks remain cached, when successful entries become stale, and who owns invalidation. A caller canceling its own wait should not automatically cancel shared work still needed by another caller. Test duplicate keys, different keys, failures, and invalidation while work is in flight. Sharing is useful only when lifecycle and concurrency promises are explicit.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var cache = new Dictionary<string, Task<int>>(StringComparer.Ordinal);
int reads = 0;
Task<int> first = Get("rune");
Task<int> second = Get("rune");
Console.WriteLine(ReferenceEquals(first, second));
Console.WriteLine(string.Join(",", await Task.WhenAll(first, second)));
Console.WriteLine(reads);
Task<int> Get(string key) {
    if (cache.TryGetValue(key, out var existing)) return existing;
    Task<int> created = Read(key);
    cache.Add(key, created);
    return created;
}
async Task<int> Read(string key) {
    reads++;
    await Task.Yield();
    return key.Length;
}
Microsoft documentation ↗
Data

JSON presence can be a three-way contract

Absent, explicit null, and a supplied value can mean different commands.

Read explanation & example

A JSON object can omit a property, include it with null, or include an ordinary value such as zero. Those states are distinct. TryGetProperty checks presence without throwing for absence; ValueKind then describes the value of a property that exists. A partial update may interpret omission as leave unchanged and null as clear. Deserializing too early into an ordinary nullable property can lose the presence distinction if the model uses the same null for both cases. Preserve meaningful states until the domain boundary intentionally combines or rejects them. After finding a property, still validate its expected kind and range. A present number need not fit int, and a present string need not satisfy a nonblank-name rule. Test missing, null, valid, and wrong-kind payloads separately. A useful parser explains structural absence and invalid supplied content precisely instead of substituting one default value that hides their different meanings.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

using var document = JsonDocument.Parse("""{"present":null,"number":0}""");
JsonElement root = document.RootElement;
Console.WriteLine(root.TryGetProperty("missing", out _));
Console.WriteLine(root.GetProperty("present").ValueKind);
Console.WriteLine(root.TryGetProperty("number", out var number));
Console.WriteLine(number.GetInt32());
Microsoft documentation ↗
Data

Required JSON members still need value rules

Presence metadata and nonnull domain content are separate guarantees.

Read explanation & example

System.Text.Json can enforce required property presence through model metadata. Deserializing an empty object into the lesson's required Name model fails. A payload that explicitly supplies Name as null satisfies presence under the ordinary options used here, so separate value validation is still necessary. Do not treat a C# nonnullable annotation as a universal runtime input validator. Serializer nullability options, constructor requirements, and domain rules have their own configuration and version-specific behavior. Select and test the intended combination at the application's boundary. Use a table containing missing, null, empty, whitespace-only, and accepted names. Distinguish parsing or structural failure from a model that exists but has invalid content. Also avoid mapping every JsonException to missing Name, because malformed syntax and incompatible value kinds can fail through the same broad exception type. A complete contract states presence, accepted representation, null handling, and meaningful content independently.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

try { JsonSerializer.Deserialize<Profile>("{}"); }
catch (JsonException) { Console.WriteLine("missing required"); }
var profile = JsonSerializer.Deserialize<Profile>("""{"Name":null}""")!;
Console.WriteLine(profile.Name is null);
class Profile { public required string Name { get; init; } }
Microsoft documentation ↗
Data

CSV needs a quoting state

A delimiter inside quotes belongs to the field, not to the column boundary.

Read explanation & example

Blindly splitting a CSV row at every comma cannot preserve a quoted comma. A parser must track whether it is inside a quoted field and whether doubled quotes represent one literal quote. The supplied example accepts a bounded single-row grammar and rejects line breaks, unclosed quotes, and invalid text after a closing quote. That scope is intentional. A file-level CSV implementation may need multiline records, dialect settings, encoding handling, size limits, and source locations. The linked .NET parsing documentation illustrates a more complete parser surface; the browser sample itself uses only local strings and does not require that file-based API. Test empty fields, a trailing delimiter, a quoted delimiter, doubled quotes, and malformed input. Then validate column count and domain values in later stages. Keeping grammar errors separate from invalid numbers prevents a malformed record from silently shifting values into the wrong columns while still looking superficially plausible.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

Console.WriteLine(string.Join("|", Parse("Ada,\"a,b\",\"say \"\"hi\"\"\"")));
static List<string> Parse(string line) {
    var fields = new List<string>(); int index = 0;
    while (true) {
        var field = new StringBuilder();
        if (index < line.Length && line[index] == '"') {
            index++; bool closed = false;
            while (index < line.Length) {
                char c = line[index++];
                if (c == '"') {
                    if (index < line.Length && line[index] == '"') { field.Append('"'); index++; }
                    else { closed = true; break; }
                }
                else if (c is '\r' or '\n') throw new FormatException("Single row only");
                else field.Append(c);
            }
            if (!closed || (index < line.Length && line[index] != ',')) throw new FormatException("Invalid quoted field");
        } else {
            while (index < line.Length && line[index] != ',') {
                char c = line[index++];
                if (c is '"' or '\r' or '\n') throw new FormatException("Invalid unquoted field");
                field.Append(c);
            }
        }
        fields.Add(field.ToString());
        if (index == line.Length) return fields;
        index++;
    }
}
Microsoft documentation ↗
Data

Stable JSON order has a limited scope

A sorted map export is reproducible without being a universal canonicalization scheme.

Read explanation & example

A SortedDictionary with an explicit ordinal comparer supplies deterministic key enumeration for the lesson's string-to-integer map. Serializing equivalent maps built in different insertion orders therefore produces the same text. This is useful for readable diffs and bounded snapshot fixtures. Semantic equality and textual equality are different questions. JSON object order should not normally decide domain meaning, while reproducible text may still help humans review changes. A complete canonical JSON scheme needs additional rules for nested structures, numbers, escaping, duplicate names, and other representation details. Do not use this helper as a cryptographic signing format merely because two local examples match. Test different insertion orders, changed values, empty maps, and any nested shape the application actually supports. Name the limited invariant clearly: stable property ordering for a specified model and serializer configuration. A precise narrow promise is more useful than an impressive name that implies an unspecified standard.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var first = new Dictionary<string, int> { ["b"] = 2, ["a"] = 1 };
var second = new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 };
string Stable(Dictionary<string, int> values) => JsonSerializer.Serialize(
    new SortedDictionary<string, int>(values, StringComparer.Ordinal));
Console.WriteLine(Stable(first));
Console.WriteLine(Stable(first) == Stable(second));
Microsoft documentation ↗
Algorithms

Lower bound returns a boundary position

A successful insertion boundary can equal the array length.

Read explanation & example

Lower bound finds the first sorted-array position whose value is not less than the target. It uses a half-open candidate interval, so Length is a valid final position even though it is not an index callers may read. Membership requires a separate bounds check and equality comparison after the search. This custom boundary contract differs from asking Array.BinarySearch for any matching element. With duplicates, the lesson deliberately returns the first occurrence. An upper-bound variant changes the comparison to find the first greater element, allowing duplicate counts from the difference between the two boundaries. Test empty input, all-equal input, a target below every value, and a target above every value. Trace low and high to verify that the interval shrinks without losing the answer. Both variants require input sorted under the same comparison policy used by the search. A boundary result should not be treated as a readable slot until validated.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int[] values = { 1, 3, 3, 7 };
Console.WriteLine(LowerBound(values, 3));
Console.WriteLine(LowerBound(values, 5));
Console.WriteLine(LowerBound(values, 9));
static int LowerBound(int[] values, int target) {
    int low = 0, high = values.Length;
    while (low < high) {
        int middle = low + (high - low) / 2;
        if (values[middle] < target) low = middle + 1;
        else high = middle;
    }
    return low;
}
Microsoft documentation ↗
Algorithms

Priority queues can contain superseded work

Dijkstra can enqueue an improvement and ignore an older proposal later.

Read explanation & example

PriorityQueue removes an element with the smallest priority, but it does not automatically replace an older entry when another entry describes the same logical node. Dijkstra can therefore enqueue an improved distance while leaving an obsolete proposal in the queue. Compare a dequeued cost with the current best distance and skip stale proposals. The shortest-path contract requires nonnegative edge costs. The sample rejects negatives and checks arithmetic rather than allowing an overflowing path cost to wrap into a misleading small priority. Final distances are sorted separately for display because equal-priority dequeue order is not a stable presentation guarantee. Test a graph where a node first receives an expensive route and later a cheaper route, plus disconnected and zero-cost cases. The heap supplies efficient priority selection; the distance map and stale-entry invariant establish which proposals are still meaningful. Container choice does not remove the need to state algorithm preconditions and unreachable-result policy.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var graph = new Dictionary<string, (string To, int Cost)[]> {
    ["A"] = new[] { ("B", 4), ("C", 1) }, ["B"] = new[] { ("D", 1) },
    ["C"] = new[] { ("B", 2), ("D", 5) }, ["D"] = Array.Empty<(string, int)>()
};
if (graph.Values.SelectMany(edges => edges).Any(edge => edge.Cost < 0)) throw new ArgumentException("Negative edge");
var distance = graph.Keys.ToDictionary(key => key, _ => int.MaxValue, StringComparer.Ordinal);
var queue = new PriorityQueue<string, int>();
distance["A"] = 0; queue.Enqueue("A", 0);
while (queue.TryDequeue(out string? node, out int cost)) {
    if (cost != distance[node]) continue;
    foreach (var edge in graph[node]) {
        int candidate = checked(cost + edge.Cost);
        if (candidate < distance[edge.To]) {
            distance[edge.To] = candidate; queue.Enqueue(edge.To, candidate);
        }
    }
}
Console.WriteLine(string.Join(",", distance.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}:{pair.Value}")));
Microsoft documentation ↗
Algorithms

A sorted ready set makes schedules reproducible

Choose among ready nodes without changing the dependency rule.

Read explanation & example

Topological scheduling can have several valid next nodes whose prerequisite counts are zero. A SortedSet chooses among those ready names using an explicit comparer. The lesson uses ordinal ordering to produce deterministic output, while incoming-edge counts enforce the actual dependency relationship. A sorted set also treats comparer-equal values as the same set member. Choose a comparer consistent with the graph's identifier policy; otherwise a case-insensitive ready set beside a case-sensitive node dictionary could collapse distinct nodes accidentally. Ordering and identity policies must agree where a container uses one comparison for both. Test two independent chains, a self-loop, and a disconnected node. Verify every dependency edge against final positions, then assert exact output only if a tie policy is intentional. If processed count is smaller than declared node count, report the cycle rather than returning a partial schedule as a complete success. Determinism helps review; it is not a substitute for dependency correctness.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var graph = new Dictionary<string, string[]> {
    ["A"] = new[] { "C" }, ["B"] = new[] { "C" }, ["C"] = new[] { "D" }, ["D"] = Array.Empty<string>()
};
Console.WriteLine(string.Join(",", Sort(graph)));
try { Sort(new Dictionary<string, string[]> { ["A"] = new[] { "B" }, ["B"] = new[] { "A" } }); }
catch (InvalidOperationException) { Console.WriteLine("cycle"); }
static List<string> Sort(Dictionary<string, string[]> graph) {
    var degree = graph.Keys.ToDictionary(key => key, _ => 0, StringComparer.Ordinal);
    foreach (var neighbors in graph.Values) foreach (string next in neighbors) degree[next]++;
    var ready = new SortedSet<string>(degree.Where(pair => pair.Value == 0).Select(pair => pair.Key), StringComparer.Ordinal);
    var result = new List<string>();
    while (ready.Count > 0) {
        string node = ready.Min!; ready.Remove(node); result.Add(node);
        foreach (string next in graph[node]) if (--degree[next] == 0) ready.Add(next);
    }
    if (result.Count != graph.Count) throw new InvalidOperationException("Dependency cycle");
    return result;
}
Microsoft documentation ↗
Algorithms

Represent missing algorithm results explicitly

Avoid a numeric sentinel that can also be a legitimate answer.

Read explanation & example

An algorithm returning a number sometimes also needs to represent no answer. A sentinel such as negative one is ambiguous if negative one is a valid result. The next-greater example uses int? so null means no later greater value, while every integer remains available as a genuine answer. The output layer formats null as none only when presenting the result. Internal logic retains the distinction instead of forcing callers to infer absence from a special numeric convention. This makes the same algorithm work with negative as well as positive input values. Test a value whose true next-greater answer is negative one alongside a position with no answer. Both should remain distinguishable before formatting. Nullable values do not replace every result model: an operation with several failure categories may need a richer result type. Choose the smallest representation that preserves every meaningful outcome, and document whether absence is ordinary or indicates invalid input.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int[] values = { 2, 1, 4, 3 };
int?[] next = new int?[values.Length];
var pending = new Stack<int>();
for (int index = 0; index < values.Length; index++) {
    while (pending.Count > 0 && values[pending.Peek()] < values[index]) next[pending.Pop()] = values[index];
    pending.Push(index);
}
Console.WriteLine(string.Join(",", next.Select(value => value?.ToString(CultureInfo.InvariantCulture) ?? "none")));
Microsoft documentation ↗
Methods

Optional defaults belong to the caller contract

Changing an optional default does not update an already compiled caller.

Read explanation & example

Optional argument values make a call concise, but their defaults are generally supplied by the compiler at the call site. If a library changes an optional constant later, a previously compiled client can continue passing the old value until it is rebuilt. Use an explicit policy object or overload that reads current configuration when the value must change dynamically. Named arguments also depend on parameter names, so renaming a public parameter can affect source compatibility even when its position and type remain the same. The sample keeps the default visible by printing an omitted value and an explicit override; it does not simulate separate library compilation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

string Describe(string name, int copies = 1) => $"{name}:{copies}";
Console.WriteLine(Describe("rune"));
Console.WriteLine(Describe(name: "rune", copies: 3));
Microsoft documentation ↗
Methods

A params array may still belong to the caller

Convenient variable-argument syntax does not transfer ownership of an existing array.

Read explanation & example

An array-based params parameter is an ordinary array inside the method. Separate arguments use the convenient call syntax, but a caller may also supply an existing array directly. In that case the method receives a reference to that same container. Mutating its elements changes the caller's data. Prefer reading the values or make an independent copy before a transformation that requires ownership. Define the zero-argument contract as well: an empty total can sensibly be zero, while an empty average needs another policy. The example intentionally mutates a supplied array to expose the aliasing; it is an observation of the contract, not a recommendation for a surprising API.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

void ReplaceFirst(params int[] values) { if (values.Length > 0) values[0] = 99; }
int[] original = { 1, 2 };
ReplaceFirst(original);
Console.WriteLine(string.Join(",", original));
Microsoft documentation ↗
Methods

An out value needs its success discriminator

A failure result and a legitimate zero can contain the same numeric value.

Read explanation & example

A Try-pattern method returns a success flag separately from an out result. Definite assignment guarantees that the method assigns the out parameter before returning normally, but it does not make the failure value useful as a successful result. Integer TryParse writes zero when parsing fails; parsing a legitimate zero also produces zero. Therefore the caller must inspect the boolean rather than infer success from the number. Apply domain validation after conversion: successful parsing of a negative age still does not make that age acceptable. The sample shows two zero-valued outputs whose flags differ, making the distinction directly observable.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

bool first = int.TryParse("0", NumberStyles.Integer, CultureInfo.InvariantCulture, out int zero);
bool second = int.TryParse("bad", NumberStyles.Integer, CultureInfo.InvariantCulture, out int failed);
Console.WriteLine($"{first}:{zero}");
Console.WriteLine($"{second}:{failed}");
Microsoft documentation ↗
Methods

Ref aliases a variable, not merely an object

Object mutation and replacement of the caller’s variable are distinct operations.

Read explanation & example

Ordinary parameter passing copies a value. When that value is an object reference, caller and callee can still refer to the same mutable object, but assigning a different reference to the parameter does not replace the caller's variable. A ref parameter instead aliases that variable and permits replacement to be observed by the caller. Both declaration and call mark the relationship explicitly. Prefer a return value when it expresses the result more clearly; ref is a deliberate contract rather than a general requirement for mutating objects. The example replaces a local reference first and then replaces the actual caller variable through ref.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

void LocalReplace(List<int> values) { values = new List<int> { 8 }; }
void CallerReplace(ref List<int> values) { values = new List<int> { 9 }; }
var numbers = new List<int> { 1 };
LocalReplace(numbers); Console.WriteLine(numbers[0]);
CallerReplace(ref numbers); Console.WriteLine(numbers[0]);
Microsoft documentation ↗
Types

Tuple names improve source readability

Element names do not create a separate nominal tuple type.

Read explanation & example

Named tuple elements make compact results easier to read, but the names are not a distinct nominal domain type. Compatible tuples are primarily determined by their element types and positions. Assignment can therefore expose the same values under different element names. Use a tuple for a small, local group of related results; prefer a named record or class when a result has important invariants, travels through many layers, or needs its own behavior. Copying a tuple also copies its element values, so a mutable reference stored as one element can still be shared. The sample demonstrates compatible names without claiming any deep-copy guarantee.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

(int Low, int High) bounds = (2, 8);
(int Start, int End) span = bounds;
Console.WriteLine($"{span.Start}:{span.End}");
var (first, last) = bounds;
Console.WriteLine(last - first);
Microsoft documentation ↗
Types

The zero flag is always contained

HasFlag(None) does not test whether a flags value is empty.

Read explanation & example

HasFlag checks whether all bits in the supplied flag are present. A zero flag requires no bits, so HasFlag with a zero-valued None member returns true for every flags value. Test equality with None when the question is whether no flags are set. Give independent basic flags powers of two, and reject unknown bits at an input boundary if only a known mask is valid. The Flags attribute helps display combinations but does not enforce the application's allowed combinations. The sample contrasts testing containment of zero with testing equality to zero, which produce different answers for a nonempty permission value.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

Access value = Access.Read;
Console.WriteLine(value.HasFlag(Access.None));
Console.WriteLine(value == Access.None);
Console.WriteLine((value & ~Access.All) == 0);
[Flags] enum Access { None = 0, Read = 1, Write = 2, All = Read | Write }
Microsoft documentation ↗
Collections

An empty queue is an ordinary outcome

TryDequeue combines removal with an explicit empty-result signal.

Read explanation & example

A queue models first-in, first-out order, but no item is guaranteed to exist at the moment a caller asks for one. TryDequeue returns false for an empty ordinary queue instead of throwing. Consume the out result only when the returned flag is true. This API shape is useful for a single-threaded processing loop that naturally ends when no items remain. It does not make Queue thread-safe or turn a separate Count check into an atomic concurrency protocol. If multiple threads share work, choose a collection and synchronization design intended for that requirement. The example shows the successful removal and the subsequent empty outcome explicitly.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var queue = new Queue<string>();
queue.Enqueue("first");
Console.WriteLine(queue.TryDequeue(out string? item));
Console.WriteLine(item);
Console.WriteLine(queue.TryDequeue(out _));
Microsoft documentation ↗
Collections

History needs independent state

Pushing the same mutable reference does not record a past snapshot.

Read explanation & example

A stack provides last-in, first-out order, but it does not clone the objects pushed onto it. If a history entry references a list and the list changes later, popping that entry reveals the changed list rather than the old state. Store immutable values, explicit independent copies, or inverse operations when implementing undo. The right choice depends on the operation's size and domain contract. A shallow container copy is sufficient for integer elements but not necessarily for mutable objects inside it. The sample stores both a shared list and a copied list, then mutates the source so the ownership difference can be seen directly.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var source = new List<int> { 1 };
var aliases = new Stack<List<int>>();
var snapshots = new Stack<List<int>>();
aliases.Push(source); snapshots.Push(source.ToList());
source.Add(2);
Console.WriteLine(aliases.Pop().Count);
Console.WriteLine(snapshots.Pop().Count);
Microsoft documentation ↗
Collections

Hash keys must keep stable equality state

Changing fields used for hashing can break later key lookup.

Read explanation & example

A hash-based collection uses a hash code to choose a search bucket and equality to distinguish candidates inside it. Equal values must have equal hash codes, while unequal values may still collide. A key whose equality or hash-relevant fields change after insertion can end up logically belonging in a different bucket, breaking later lookup assumptions. Prefer immutable identifiers for dictionary and set keys. Do not persist a hash code as a durable identifier or assume it is unique. The sample uses an immutable record identifier and verifies logical equality through HashSet membership without printing implementation-dependent hash values.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var known = new HashSet<ProductId> { new ProductId("A1") };
Console.WriteLine(known.Contains(new ProductId("A1")));
Console.WriteLine(known.Contains(new ProductId("B2")));
record ProductId(string Code);
Microsoft documentation ↗
Collections

A missing binary search encodes an insertion index

Complement a negative result instead of treating it as an ordinary index.

Read explanation & example

List.BinarySearch requires a list sorted with the same comparison rule used for the search. A found item produces a nonnegative index. A missing item produces the bitwise complement of the position where insertion preserves sorted order; applying ~ to that negative result recovers the insertion index. Do not complement a successful index, and do not insert using the unconverted negative value. When duplicate values exist, a successful search does not promise the first occurrence. If the domain needs a lower or upper bound, specify that additional requirement. The example inserts a missing middle value after decoding the negative result.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var values = new List<int> { 2, 4, 8 };
int result = values.BinarySearch(6);
Console.WriteLine(result < 0);
int position = result < 0 ? ~result : result;
values.Insert(position, 6);
Console.WriteLine(string.Join(",", values));
Microsoft documentation ↗
LINQ

Zip discards the unmatched tail

Successful positional pairing does not prove that every input element was represented.

Read explanation & example

Zip pairs elements at matching positions and ends when either sequence ends. A longer input's remaining elements do not produce rows and do not trigger a length-mismatch exception. That is appropriate when the operation intentionally uses the overlapping prefix. If every label needs a value, validate the alignment contract before zipping or store each label and value together in one record. Equal lengths alone also cannot prove semantic alignment after one input has been independently sorted or filtered. The sample exposes the shorter-result behavior with three labels and two values, then prints the unmatched count calculated from the prepared array lengths.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

string[] names = { "A", "B", "C" };
int[] values = { 1, 2 };
var pairs = names.Zip(values, (name, value) => $"{name}:{value}").ToArray();
Console.WriteLine(string.Join(",", pairs));
Console.WriteLine(names.Length - pairs.Length);
Microsoft documentation ↗
LINQ

Join returns every matching pair

Duplicate keys on both sides can multiply result rows.

Read explanation & example

An inner join produces one output for each pair whose selected keys compare equal. A left row with no match contributes no output. If one left row has several matching right rows, it produces several results; if duplicate keys exist on both sides, those combinations multiply. Validate reference-key uniqueness when a later total assumes a one-to-one or one-to-many relationship. A join's method signature does not enforce that business invariant. Keep an unmatched-record report when silent omission would hide import errors. The sample joins two left rows and three right rows sharing one key, producing six combinations rather than three.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var left = new[] { (Key: "A", Value: 1), (Key: "A", Value: 2) };
var right = new[] { (Key: "A", Value: 3), (Key: "A", Value: 4), (Key: "A", Value: 5) };
Console.WriteLine(left.Join(right, x => x.Key, x => x.Key, (a, b) => (a.Value, b.Value)).Count());
Microsoft documentation ↗
Collections

RemoveAll mutates one list deliberately

Use a pure predicate and read the returned removed-item count.

Read explanation & example

RemoveAll removes the elements of a List that satisfy a predicate and returns how many elements were removed. Surviving items retain their relative order. It is a clearer alternative to structural removal during foreach enumeration, but it still mutates the same list object that other aliases can observe. Use Where followed by ToList when the requirement is an independent filtered result. The predicate should make a decision about one item rather than mutate the same list, perform unrelated work, or depend on changing traversal state. The example removes negative values, prints the removal count, and then prints the remaining sequence to verify both effects.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var values = new List<int> { 1, -1, 2, -2, 0 };
int removed = values.RemoveAll(x => x < 0);
Console.WriteLine(removed);
Console.WriteLine(string.Join(",", values));
Microsoft documentation ↗
Text and data

Escape a URI component once

Data characters must not accidentally become query separators.

Read explanation & example

A query value can contain ampersands, equals signs, spaces, or hash signs that also have roles in URI syntax. Escape the value as one component before combining it with the fixed structural separators. Escaping the entire finished query would also encode separators that should remain syntax. Repeatedly escaping an already encoded component can encode its percent signs and change the value again. URI escaping is a representation rule, not encryption or validation that a destination is trustworthy. The sample builds only a prepared component and round-trips it locally, without sending a network request.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

string value = "a=b&c";
string encoded = Uri.EscapeDataString(value);
Console.WriteLine(encoded);
Console.WriteLine(Uri.UnescapeDataString(encoded));
Microsoft documentation ↗
Text and data

Encoded byte counts differ from string length

Choose the correct unit when enforcing a length limit.

Read explanation & example

String.Length counts UTF-16 code units. UTF-8 byte length counts bytes in another representation, and neither is necessarily the number of user-perceived characters. Accented letters, supplementary Unicode scalars, and combining sequences expose these distinctions. A payload-size limit needs an encoded byte count; a user-interface limit may need a text-element policy. Avoid truncating at an arbitrary byte or UTF-16 position because it can split an encoded character or surrogate pair. The sample compares an ASCII letter, an accented letter, and a supplementary symbol, printing both measures without pretending that either is a complete grapheme count.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

foreach (string text in new[] { "A", "é", "😀" })
    Console.WriteLine($"{text.Length}:{Encoding.UTF8.GetByteCount(text)}");
Microsoft documentation ↗
Dates and time

A calendar date is not a universal instant

Do not attach an arbitrary midnight or zone to a date-only domain value.

Read explanation & example

DateOnly represents a calendar date without a time of day or time-zone interpretation. It fits values such as an invoice date or a birth date when the domain does not intend an instant on a universal timeline. DateTimeOffset answers a different question by combining a clock reading with an offset. Converting a date-only value into a scheduled instant requires additional time and zone policy; a device default should not supply that information silently. Calendar arithmetic also differs from adding a fixed number of elapsed hours across a time-zone transition. The sample demonstrates a leap-year calendar boundary without introducing any ambient clock or local-zone dependency.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var before = new DateOnly(2024, 2, 28);
Console.WriteLine(before.AddDays(1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
Console.WriteLine(before.AddDays(2).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
Microsoft documentation ↗
Object design

Construction establishes the first valid state

Validate required domain rules before an object becomes available to callers.

Read explanation & example

A constructor can establish an invariant that later operations may rely on. If a quantity must be positive, constructing an object with zero and hoping a later method repairs it creates an avoidable invalid state. Validate before assigning the public state, and ensure later mutation or copy operations preserve the same rule. Get-only properties help restrict replacement but do not make a nested mutable object immutable. Keep constructors focused on initialization and avoid calling overridable behavior that depends on uninitialized derived state. The sample accepts one valid quantity and rejects zero before any invalid Quantity instance is returned.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

Console.WriteLine(new Quantity(2).Value);
try { _ = new Quantity(0); }
catch (ArgumentOutOfRangeException) { Console.WriteLine("rejected"); }
sealed class Quantity
{
    public int Value { get; }
    public Quantity(int value)
    {
        if (value <= 0) throw new ArgumentOutOfRangeException(nameof(value));
        Value = value;
    }
}
Microsoft documentation ↗
Object design

Unsubscribe the original event handler

Keep a delegate reference when a later lifetime boundary needs to remove it.

Read explanation & example

An event subscription retains a delegate, and that delegate can retain its target object or captured variables. Remove the same handler when the listener's work ends. Rewriting a similar anonymous lambda at unsubscription time does not reliably identify the original delegate. Store it in a variable or use a named method with the intended target. Ordinary delegate invocation is synchronous, so a handler runs on the raising call path unless the publisher explicitly dispatches work elsewhere. The sample stores an Action, subscribes it, raises once, removes it, and raises again; only the first raise reaches that listener.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var publisher = new Publisher();
int calls = 0;
Action handler = () => calls++;
publisher.Changed += handler;
publisher.Raise();
publisher.Changed -= handler;
publisher.Raise();
Console.WriteLine(calls);
sealed class Publisher
{
    public event Action? Changed;
    public void Raise() => Changed?.Invoke();
}
Microsoft documentation ↗
Generics

A factory can preserve constructor requirements

Do not force parameterless construction onto a model that needs dependencies.

Read explanation & example

The new() generic constraint allows generic code to construct T through a public parameterless constructor. That is useful when such construction matches the type's contract. It becomes awkward when creating a valid instance needs an identifier, configuration, or another dependency. Accepting Func<T> lets the caller supply a factory that knows those required values, while generic coordination remains independent of the concrete constructor. A factory can still have effects or fail, so document when and how often it is invoked. The sample creates two labels through one supplied factory and demonstrates independent instances without weakening Label's required-name constructor.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

T Make<T>(Func<T> factory) => factory();
var first = Make(() => new Label("prepared"));
var second = Make(() => new Label("prepared"));
Console.WriteLine(first.Name);
Console.WriteLine(ReferenceEquals(first, second));
sealed class Label
{
    public string Name { get; }
    public Label(string name) { Name = name; }
}
Microsoft documentation ↗
Collections

Comparison zero can mean a sorted-set duplicate

A display sort key may be too weak to define uniqueness.

Read explanation & example

IComparer<T> and IComparable<T> return a sign describing order. For a SortedSet, comparison zero also means that an element is already represented, even if ordinary object equality would distinguish the two instances. A comparer that uses only priority can therefore collapse different tasks sharing that priority. Add a stable identifier tie-breaker when the set should retain distinct tasks. Conversely, deliberately grouping by priority alone may be correct for another domain. Choose the identity contract before reusing a display sorter inside a uniqueness-enforcing collection. The sample compares a priority-only set with a priority-and-code set to expose the difference.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var byPriority = Comparer<Job>.Create((a, b) => a.Priority.CompareTo(b.Priority));
var complete = Comparer<Job>.Create((a, b) =>
{
    int first = a.Priority.CompareTo(b.Priority);
    return first != 0 ? first : StringComparer.Ordinal.Compare(a.Code, b.Code);
});
Console.WriteLine(new SortedSet<Job>(new[] { new Job("A", 1), new Job("B", 1) }, byPriority).Count);
Console.WriteLine(new SortedSet<Job>(new[] { new Job("A", 1), new Job("B", 1) }, complete).Count);
record Job(string Code, int Priority);
Microsoft documentation ↗
Memory & GC

A retaining path can outlive a local name

Removing one variable does not remove every owner of its object.

Read explanation & example

Garbage collection follows reachable paths supported by the runtime. A list, cache, event delegate, or captured variable can keep an object reachable after an unrelated local is cleared. Source-level scope is not a precise collection clock, and a cycle without a reachable root is not automatically a leak in a tracing collector. Identify all owners before describing an object as eligible. The sample removes one local reference while a list still owns the item, then reports the list's own membership count after Clear. Those outputs describe program state, not collected bytes or heap locations.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

Payload? local = new Payload("kept");
var owner = new List<Payload> { local };
local = null;
Console.WriteLine(owner[0].Name);
owner.Clear();
Console.WriteLine(owner.Count);
record Payload(string Name);
Microsoft documentation ↗
Memory & GC

Generation APIs need a runtime label

CoreCLR generation architecture is not a universal C# runtime guarantee.

Read explanation & example

CoreCLR's public generations zero, one, and two support its short-lived versus longer-lived collection strategy. Survival can lead to promotion, but a generation is not age in seconds and does not grant immortality. Mono/WebAssembly and Unity profiles must not be assigned the same collector model merely because they run C#. Record the host before interpreting GetGeneration or MaxGeneration, and do not use a public generation number as a complete physical-heap classifier. The sample prints an observation without asserting a universal expected number. Its meaning belongs to the documented runtime and execution history, not to a fixed browser-versus-desktop guess.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

Console.WriteLine($"reported-maximum:{GC.MaxGeneration}");
var item = new object();
Console.WriteLine($"reported-object-generation:{GC.GetGeneration(item)}");
GC.KeepAlive(item);
Microsoft documentation ↗
Memory & GC

A pinned handle is not POH migration

End the handle lifetime without inventing a heap-location observation.

Read explanation & example

A pinned handle asks a supporting runtime to keep its target from moving while that handle is active. It also owns a retaining relationship and must be freed exactly once. CoreCLR's pinned-object heap is a separate arrangement for designated pinned allocations; pinning an existing array does not migrate it there. IsAllocated tells you whether this handle is active, not the target's region or physical address. The example uses only managed APIs and never calls native code or obtains a pointer. A complete interop design would additionally need the actual platform contract, borrowing rules, and supported pinning lifetime.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

byte[] bytes = { 2, 4 };
var pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try { Console.WriteLine(pin.IsAllocated); }
finally { pin.Free(); }
Console.WriteLine(pin.IsAllocated);
Microsoft documentation ↗
Memory & GC

Use the target acquired by TryGetTarget

A weak reference provides optional access, not guaranteed retention.

Read explanation & example

TryGetTarget supplies a usable strong reference when the weak target is available. Consume that out reference instead of performing a separate test and another unrelated lookup. A weak reference does not decide a cache's miss policy, guarantee an object survives, or dispose a scarce resource. When all strong owners end, target availability still depends on collection timing and other runtime details, so an immediate false result is not a portable lifetime assertion. This sample deliberately retains a strong owner through KeepAlive, making the successful name observation meaningful without manufacturing collection pressure or a finalizer schedule.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

var owner = new Payload("rune");
var weak = new WeakReference<Payload>(owner);
if (weak.TryGetTarget(out var value)) Console.WriteLine(value.Name);
else Console.WriteLine("missing");
GC.KeepAlive(owner);
record Payload(string Name);
Microsoft documentation ↗
Memory & GC

Dispose is an owned cleanup protocol

A disposed managed object can still be referenced afterward.

Read explanation & example

IDisposable gives callers a deterministic protocol for ending owned work or releasing a resource. It is not a command to reclaim the object's managed storage. A using scope invokes that protocol during ordinary managed unwinding, while finalization is a nondeterministic fallback with separate restrictions. Prefer suitable SafeHandle ownership for real native handles rather than inventing finalizers casually. The sample intentionally owns only a visible in-memory state flag, then observes it through the still-existing object reference. That proves its protocol ran; it does not measure garbage collection or operating-system resource release.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

var lease = new Lease();
using (lease) { Console.WriteLine(lease.IsDisposed); }
Console.WriteLine(lease.IsDisposed);
sealed class Lease : IDisposable
{
    public bool IsDisposed { get; private set; }
    public void Dispose() => IsDisposed = true;
}
Microsoft documentation ↗
Memory & GC

A rented buffer has capacity and a used prefix

Never expose uninitialized capacity or keep using a returned lease.

Read explanation & example

ArrayPool.Rent returns at least the requested length and may provide previously used storage. Track the logical payload separately, initialize every byte you read, and return the lease once to its pool after all users finish. A reference or span escaping past Return can interfere with another borrower. A clearing request controls the pool's retained-storage policy; sensitive-data requirements may need an explicit overwrite under the owner's contract. The sample writes the entire four-byte logical payload and decodes only that prefix. It returns an independent string observation and does not claim pooling eliminates every allocation or improves this tiny workload.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(4);
try
{
    int used = Encoding.UTF8.GetBytes("rune", 0, 4, buffer, 0);
    Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, used));
}
finally { pool.Return(buffer, clearArray: true); }
Microsoft documentation ↗
Memory & GC

A stored callback can own captured data

Trace delegate storage through its captured environment.

Read explanation & example

A lambda can capture variables from its enclosing invocation and keep their environment available through a stored delegate. The captured variable is not necessarily a frozen value; later assignments within that environment can change what the delegate reads. A long-lived callback registry can therefore retain more data than its public signature reveals. Decide which component ends that registration, and consider explicit parameters or static lambdas when capture is unnecessary. The sample demonstrates a real captured variable with a changed value. It does not expose compiler-generated class names or pretend that one allocation shape is guaranteed across all optimizers.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

Func<string> Build()
{
    string label = "before";
    Func<string> callback = () => label;
    label = "retained";
    return callback;
}
var callback = Build();
Console.WriteLine(callback());
Microsoft documentation ↗
Memory & GC

Allocation traffic is not retained memory

Keep the metric definition and target next to every byte observation.

Read explanation & example

GetAllocatedBytesForCurrentThread reports managed allocation attributed to one thread, not surviving storage or process memory. A synchronous before-and-after delta can investigate a bounded interval where the API is supported. An await may change the relevant thread in some runtimes, and native allocations are outside this counter. Warmup, optimization, and instrumentation also matter. The sample keeps a small array observably live and prints a target-specific delta without a fixed expected number. Unsupported reporting remains explicit. Compare browser and desktop observations only with their runtime labels intact, and verify correctness before interpreting a smaller allocation number as an improvement.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

try
{
    long before = GC.GetAllocatedBytesForCurrentThread();
    var payload = new byte[32];
    long after = GC.GetAllocatedBytesForCurrentThread();
    Console.WriteLine($"observed:{after - before}");
    GC.KeepAlive(payload);
}
catch (PlatformNotSupportedException) { Console.WriteLine("counter-unavailable"); }
Microsoft documentation ↗
Unity

Unity initialization and activation lifetimes

Awake/Start and repeated enable transitions have different responsibilities.

Read explanation & example

For one initially active enabled MonoBehaviour, initialization and activation callbacks have documented relationships. Awake prepares instance state; OnEnable follows an active enabled transition; Start runs once before ordinary frame work. A later disable/enable transition can repeat OnDisable and OnEnable without repeating Start. Do not infer a guaranteed cross-object order from one observed Console trace. The accompanying code is a portable activation ledger, not a Unity scheduler. It counts one initialization, one first start and two activations by explicit C# calls. Run LifecycleProbe in the actual Editor to observe real callbacks. Keep shared dependencies, event subscriptions and pooled resets assigned to the correct lifetime. An initially inactive object and disabled scene-reload settings deserve their own tests.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var session = new ActivationSession();
session.Activate(); session.Deactivate(); session.Activate();
Console.WriteLine($"initialized:{session.Initializations}");
Console.WriteLine($"started:{session.Starts}");
Console.WriteLine($"activations:{session.Activations}");
sealed class ActivationSession {
    private bool initialized, started, active;
    public int Initializations { get; private set; } public int Starts { get; private set; } public int Activations { get; private set; }
    public void Activate() { if (active) return; if (!initialized) { initialized = true; Initializations++; } active = true; Activations++; if (!started) { started = true; Starts++; } }
    public void Deactivate() { active = false; }
}
Unity documentation ↗
Unity

Force modes and the physics step

Do not apply deltaTime twice to a force API that integrates its mode.

Read explanation & example

Rigidbody.AddForce interprets its argument according to ForceMode. Continuous Force and Acceleration modes participate in physics-step integration, while their mass dependence differs. Place the intended Rigidbody work in the physics loop and keep direct Transform edits out of the same dynamic-body controller unless a deliberately supported design requires them. The portable code compares elementary acceleration and force-over-mass arithmetic. It does not solve collisions or reproduce Unity simulation. In the Editor, compare masses with one mode held fixed, then change the mode in a separate test. A callback count is not elapsed time, FixedUpdate need not occur once per rendered frame, and measured Player behavior needs the real configured physics environment.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

decimal Integrate(decimal acceleration) { decimal speed = 0; for (int i = 0; i < 2; i++) speed += acceleration * 0.5m; return speed; }
Console.WriteLine(Integrate(4m).ToString("0.00", CultureInfo.InvariantCulture));
Console.WriteLine(Integrate(4m / 2m).ToString("0.00", CultureInfo.InvariantCulture));
Unity documentation ↗
Unity

Input actions need an enable owner

A private action and a shared action map have different lifecycle policies.

Read explanation & example

The real course example creates one InputAction owned by its component, enables and subscribes in OnEnable, removes its handler and disables in OnDisable, then disposes the owned action. That policy should not be copied blindly to a shared action map used by other components. Disabling shared input can affect an unrelated user interface or controller. Install a compatible Input System package and configure Active Input Handling before expecting the script to compile and receive device events. The portable snippet only counts Boolean rising edges and prints two. It does not read a keyboard. Test fresh presses with Game view focused and repeat enable transitions to catch duplicated subscriptions; package configuration and event lifetime are separate checks.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var edges = new PressEdges(); int presses = 0;
foreach (bool down in new[] { false, true, true, false, true }) if (edges.Observe(down)) presses++;
Console.WriteLine(presses);
sealed class PressEdges { private bool previous; public bool Observe(bool down) { bool pressed = down && !previous; previous = down; return pressed; } }
Unity documentation ↗
Unity

Shared assets are not independent health state

Use ScriptableObject configuration to construct each actor’s own mutable model.

Read explanation & example

Several GameObjects can refer to one ScriptableObject asset. Changing a mutable field on that asset changes shared data rather than creating a private actor snapshot. Keep authored starting values on the shared profile and allocate or reset per-instance runtime state under an explicit owner. The portable example constructs two health states from one ordinary configuration object, damages the first and prints seven, ten and ten. The Unity EnemyProfile example uses an actual ScriptableObject asset for the corresponding ownership experiment. Do not assume Play mode asset edits behave like temporary scene-instance edits, and do not use assets as a replacement for a versioned save system. Pooling also needs an intentional reset of runtime state between leases.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var profile = new Profile(10); var first = profile.Create(); var second = profile.Create();
first.Damage(3); Console.WriteLine(first.Current); Console.WriteLine(second.Current); Console.WriteLine(profile.StartingHealth);
sealed class Profile { public int StartingHealth { get; } public Profile(int health) { StartingHealth = health; } public Health Create() => new Health(StartingHealth); }
sealed class Health { public int Current { get; private set; } public Health(int health) { Current = health; } public void Damage(int amount) { Current = Math.Max(0, Current - amount); } }
Unity documentation ↗
Unity

Unity null checks and managed wrappers

The overloaded Unity comparison is not the same as ?. or ReferenceEquals.

Read explanation & example

A destroyed UnityEngine.Object can leave a managed wrapper reference that Unity compares equal to null. ReferenceEquals can still report a nonnull managed reference. The C# null-conditional and null-coalescing operators do not invoke that overloaded equality as a universal destroyed-object guard. The portable code demonstrates only ordinary object aliases; it deliberately cannot emulate Unity’s native-object relationship. Use the real DestroyProbe with a separate target to see the actual distinction after destruction has been processed. Destroying the object hosting the observation coroutine would invalidate that experiment. Keep native resource destruction, managed reachability, and variable reassignment conceptually separate, especially when storing engine references in events, caches or delayed callbacks.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

object? owner = new object(); object alias = owner; owner = null;
Console.WriteLine(alias == null); Console.WriteLine(ReferenceEquals(owner, null));
Unity documentation ↗
Unity

Pooling needs one return authority

Reset before activation and avoid release/OnDisable recursion.

Read explanation & example

A pool can reuse instances without resetting their gameplay state automatically. Configure state before SetActive(true) when OnEnable consumes that state. An inactive prefab root prevents creation from activating before the pool prepares the instance. Release can deactivate the instance, but an OnDisable handler that calls Release again can create a recursive lifecycle loop. The C# companion reuses a plain token, resets health and counts one creation. It does not measure Unity allocations or implement UnityEngine.Pool. The real pool example owns its instances under its transform and uses one explicit rent/return action. For gameplay leases, define duplicate-return protection, outstanding-instance teardown, and subscription/timer cleanup; reuse changes ownership obligations rather than removing them.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var cache = new LeaseCache(); var first = cache.Rent(); first.Health = 3; cache.Return(first); var second = cache.Rent();
Console.WriteLine(ReferenceEquals(first, second)); Console.WriteLine(second.Health); Console.WriteLine(cache.Created);
sealed class Token { public int Health = 10; }
sealed class LeaseCache { private readonly Stack<Token> free = new(); public int Created { get; private set; } public Token Rent() { if (free.Count > 0) return free.Pop(); Created++; return new Token(); } public void Return(Token token) { token.Health = 10; free.Push(token); } }
Unity documentation ↗
Unity

Unity GC and CoreCLR are different targets

Incremental non-generational collection does not provide desktop generation-zero assumptions.

Read explanation & example

The Unity6.0 documentation describes its Boehm-based non-generational collector and incremental operation where supported. That is different from CoreCLR’s commonly discussed generational model. A temporary allocation policy should be verified on the actual Unity backend and Player, not justified by assuming every short-lived object receives a cheap young-generation collection. The portable example calculates cumulative requested allocation volume from supplied bytes per frame. It measures neither live memory nor collector duration. The Unity configuration probe separately reports actual incremental status and a managed-memory estimate, which is not total native or GPU memory. Record platform, backend and capture settings, and use Profiler evidence for allocation and pause claims instead of turning one arithmetic ratio into a performance guarantee.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

long frames = 60L * 60L; long original = 1024L * frames; long reduced = 64L * frames;
Console.WriteLine(original); Console.WriteLine(reduced); Console.WriteLine(original - reduced);
Unity documentation ↗
Unity

Static state needs a session boundary

Disabled domain reload preserves assumptions that must be made explicit.

Read explanation & example

Editor domain reload and scene reload are different settings. When domain reload is disabled, static fields and event state can survive between Play sessions unless the owning service resets them. The real example uses a SubsystemRegistration runtime initialization callback for its own static counter before scene instances contribute to it. The portable code calls Reset explicitly and shows a two-instance session followed by a one-instance session. It does not change Editor settings or prove runtime initialization order. Test repeated Play entry under the actual settings and handle static event subscriptions deliberately. Clearing another service’s events is not a universal solution; identify ownership and ensure callbacks do not accumulate simply because the managed domain was reused.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

SessionCounter.Reset(); SessionCounter.Created(); SessionCounter.Created(); Console.WriteLine(SessionCounter.Count);
SessionCounter.Reset(); SessionCounter.Created(); Console.WriteLine(SessionCounter.Count);
static class SessionCounter { public static int Count { get; private set; } public static void Reset() { Count = 0; } public static void Created() { Count++; } }
Unity documentation ↗
Testing & Design

An expectation must be independent

Do not compute the expected answer by repeating the code being tested.

Read explanation & example

A check is informative when its expectation comes from the requirement, a trusted fixture, or independently justified reasoning. Calling the production method twice and comparing the results mainly observes repeatability; it does not establish the answer is correct. Copying the same branches into an expected-value function can preserve the same bug on both sides. Write small concrete expectations before running, and choose cases that distinguish plausible wrong rules. Also arrange other prerequisites valid when isolating a rejection: a full-capacity fixture cannot independently establish that an expiration rule works. The sample uses a literal false expectation for an excessive request, so a permissive predicate would create a real mismatch. This is a portable comparison, not a discovered framework test or a universal proof about every integer input.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

bool CanReserve(int requested, int available) => requested > 0 && requested <= available;
bool expected = false;
bool actual = CanReserve(4, 3);
Console.WriteLine(actual == expected);
Microsoft documentation ↗
Testing & Design

Observe the required exception type

No exception and an unrelated exception are distinct mismatches.

Read explanation & example

An exact exception expectation asks for the required failure type from the intended operation. A broad catch that accepts anything can hide a programming defect as an input-validation success. Put the action inside the observation boundary and preserve unexpected failure information in real reports. Framework APIs differ in whether an assertion accepts derived exception types, so read the selected API's contract rather than guessing from a generic name. The sample deliberately records one exception type and compares it with the literal expected name. Catching broadly is confined to that test observation; it is not recommended application recovery code. No localized message is asserted, and this small function does not provide a full test runner's diagnostics or fixture handling.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

string Observe(Action action)
{
    try { action(); return "none"; }
    catch (Exception error) { return error.GetType().Name; }
}
string observed = Observe(() => throw new ArgumentOutOfRangeException("quantity"));
Console.WriteLine(observed == nameof(ArgumentOutOfRangeException));
Microsoft documentation ↗
Testing & Design

A controlled clock is not every timer

Overriding GetUtcNow alone does not implement virtual-time scheduling.

Read explanation & example

TimeProvider supplies several related capabilities. A tiny test subclass overriding GetUtcNow can control an instant-based rule, but it does not automatically change GetTimestamp, CreateTimer, or delays that ignore the provider. Name and document such helpers according to their actual scope. Use independent clocks for independent scenarios, and test the exact deadline boundary without sleeping. A real provider-aware timer test needs the corresponding timer implementation or a supported testing utility, which is a separate dependency. The sample returns one fixed UTC instant and checks equality with it. It demonstrates a genuine .NET9 override that runs in the portable profile, while making no claim to advance an entire operating system, engine frame loop, or asynchronous scheduler.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var instant = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
TimeProvider clock = new FixedClock(instant);
Console.WriteLine(clock.GetUtcNow() == instant);
sealed class FixedClock : TimeProvider
{
    private readonly DateTimeOffset instant;
    public FixedClock(DateTimeOffset instant) { this.instant = instant; }
    public override DateTimeOffset GetUtcNow() => instant;
}
Microsoft documentation ↗
Testing & Design

Seed reproducibility has a scope

Preserve a failing input as well as the generator seed and runtime.

Read explanation & example

Two freshly initialized Random instances with the same seed can reproduce a sequence within the same implementation. That does not make the particular values a permanent cross-version data format. The order and number of draws also matter: an added call can shift every later generated case. Record the runtime, bounds, seed, draw count, and actual failing input when investigating a failure, then retain a concrete regression case. Random input generation needs an independent behavioral oracle; a random number is not an expected answer. The sample compares two locally generated sequences without hardcoding their values and checks the documented half-open range. It neither proves broad domain correctness nor claims that a short sample necessarily includes every important boundary.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

int[] Generate()
{
    var random = new Random(7);
    return Enumerable.Range(0, 5).Select(_ => random.Next(0, 4)).ToArray();
}
int[] first = Generate();
Console.WriteLine(first.SequenceEqual(Generate()));
Console.WriteLine(first.All(value => value >= 0 && value < 4));
Microsoft documentation ↗
Testing & Design

Record only relevant boundary effects

A fake can expose a notice contract without sending a real message.

Read explanation & example

A recording fake is useful when a public requirement includes an effect, such as one requested notice after acceptance and none after rejection. Inspect meaningful content and count, rather than all private calls simply because they are observable through a mock. The fake does not establish remote delivery, persistence, retries, or transactional behavior. Those belong to integration and end-to-end evidence. Keep a fake small enough that its behavior is understandable, and avoid rebuilding the real provider inside it. In the sample an Action appends to an ordinary list; there is no external communication. One accepted call records accepted and a rejected call records nothing. The observations therefore establish local effect policy, with the actual adapter still outside their scope.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var messages = new List<string>();
void Process(bool accepted, Action<string> notify) { if (accepted) notify("accepted"); }
Process(true, messages.Add);
Process(false, messages.Add);
Console.WriteLine(messages.Count);
Console.WriteLine(messages[0]);
Microsoft documentation ↗
Testing & Design

Control progress instead of sleeping

A pending task provides a causal boundary for an async check.

Read explanation & example

TaskCompletionSource lets a test owner supply a dependency's completion deliberately. A pending input prevents dependent work from finishing until that explicit outcome arrives. This is stronger than sleeping briefly and hoping a task has or has not completed. Await after completion when observing the result, rather than immediately asserting that a scheduled continuation already ran. The RunContinuationsAsynchronously option is not a promise of a particular thread identity. A controlled task can test orchestration, while a separate integration check must validate the actual external adapter. The sample starts with an incomplete task, supplies ok, and awaits that exact task. It uses genuine .NET task behavior and does not emulate a network service or add test-runner discovery to the browser.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var gate = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
Console.WriteLine(gate.Task.IsCompleted);
gate.SetResult("ok");
Console.WriteLine(await gate.Task);
Console.WriteLine(gate.Task.IsCompleted);
Microsoft documentation ↗
Testing & Design

Properties need concrete anchors

A weak relation can accept a consistently wrong implementation.

Read explanation & example

A property or metamorphic relation must be justified by the requirement and reviewed for blind spots. A constant empty normalizer can satisfy permutation invariance, duplicate invariance, and idempotence while discarding every real value. Pair useful relations with independently expected examples and preserve counterexamples when exploration finds a failure. A bounded loop checks only its stated domain; it is not automatically a mathematical proof or a full property-testing framework with generation and shrinking. The sample normalizes a local integer sequence, checks idempotence, and separately compares with a hand-written expected array. Those observations complement one another. A change in ordering or identity requirements can invalidate a previously useful relation, so revisit the property when the contract changes.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

int[] Normalize(IEnumerable<int> values) => values.Distinct().OrderBy(value => value).ToArray();
int[] normalized = Normalize(new[] { 2, 1, 2 });
Console.WriteLine(normalized.SequenceEqual(Normalize(normalized)));
Console.WriteLine(normalized.SequenceEqual(new[] { 1, 2 }));
Microsoft documentation ↗
Testing & Design

A comparison is not an installed runner

Keep portable checks separate from real framework project configuration.

Read explanation & example

A console program can compare values, print diagnostics, and return a failure exit code. It does not thereby acquire discovery, framework assertions, fixtures, standard reports, or integration with every test host. A genuine MSTest or xUnit project needs its documented SDK and dependency configuration, and dotnet test runs through the appropriate host. SharpRune's portable execution does not install that development workflow in the browser. The sample performs one actual comparison and prints PASS according to the result; it is not merely printing the expected number. Treat this as a transparent teaching check. When a production suite needs a real runner, move the domain behavior into an appropriate library and add the genuine test project rather than renaming a custom console loop as a framework.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

int Add(int left, int right) => left + right;
int actual = Add(2, 3);
int expected = 5;
Console.WriteLine(actual == expected ? "portable-check:PASS" : "portable-check:FAIL");
Microsoft documentation ↗
I/O & HTTP

A positive short read is progress

Read is a count-returning operation; a fixed-size field needs ReadExactly or a loop.

Read explanation & example

A request for four bytes can legally return two while more input remains. This card uses a real capped Stream over memory, then compares a single Read with ReadExactly. Only a zero result from a nonzero-length read identifies end-of-input in this synchronous contract; an empty destination may return zero without discovering anything about the source. ReadExactly fills its destination or throws on premature end. The custom stream returns zero for an empty request because it passes an empty slice to MemoryStream. No network transport or background thread is involved.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

using (var input = new CappedReadStream(new byte[] { 1, 2, 3, 4 })) { byte[] buffer = new byte[4]; Console.WriteLine(input.Read(buffer, 0, buffer.Length)); }
using (var input = new CappedReadStream(new byte[] { 1, 2, 3, 4 })) { byte[] header = new byte[4]; input.ReadExactly(header); Console.WriteLine(Convert.ToHexString(header)); }
using (var input = new CappedReadStream(new byte[] { 1, 2, 3, 4 })) { try { input.ReadExactly(new byte[5]); } catch (EndOfStreamException) { Console.WriteLine("truncated header"); } }
sealed class CappedReadStream : Stream {
    private readonly MemoryStream inner; public CappedReadStream(byte[] data) { inner = new MemoryStream(data, writable: false); }
    public override bool CanRead => inner.CanRead; public override bool CanSeek => false; public override bool CanWrite => false;
    public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
    public override int Read(byte[] buffer, int offset, int count) {
        ArgumentNullException.ThrowIfNull(buffer); ArgumentOutOfRangeException.ThrowIfNegative(offset); ArgumentOutOfRangeException.ThrowIfNegative(count);
        if (offset > buffer.Length - count) throw new ArgumentException("Invalid buffer range");
        return inner.Read(buffer, offset, Math.Min(count, 2));
    }
    public override int Read(Span<byte> buffer) => inner.Read(buffer.Slice(0, Math.Min(buffer.Length, 2)));
    public override void Flush() { } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    protected override void Dispose(bool disposing) { if (disposing) inner.Dispose(); base.Dispose(disposing); }
}
Microsoft documentation ↗
I/O & HTTP

Capacity does not describe the payload

A destination buffer can contain unused or stale bytes after a successful read.

Read explanation & example

The eight-byte destination begins with sentinel bytes, but the source supplies only two bytes. The read count defines the valid prefix; a span sliced to that count produces 4142. The remaining capacity is still present and must not be silently included in a decode, hash or subsequent write. This distinction also matters with rented buffers, whose contents and capacity are not a record format. The example owns an ordinary array and reports facts rather than assuming allocation or pooling behavior.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

using var input = new MemoryStream(new byte[] { 0x41, 0x42 });
byte[] buffer = Enumerable.Repeat((byte)0xCC, 8).ToArray();
int count = input.Read(buffer, 0, buffer.Length);
Console.WriteLine(count); Console.WriteLine(Convert.ToHexString(buffer.AsSpan(0, count))); Console.WriteLine(buffer.Length);
Microsoft documentation ↗
I/O & HTTP

A binary format must choose byte order

BinaryWriter integer fields are little-endian; a protocol can explicitly choose another order.

Read explanation & example

Writing an unsigned sixteen-bit value followed by an integer gives a predictable six-byte sequence because both field widths and BinaryWriter integer byte order are defined. The matching reader recovers those values, while BinaryPrimitives writes the first value in big-endian order for comparison. A successful matching writer-reader round trip alone would not prove compatibility with an external specification. Document widths, signedness, byte order and framing independently. BinaryWriter string encoding and length prefixes need their own format decisions; they are not interchangeable with fixed-width numeric fields.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

using var bytes = new MemoryStream();
using (var writer = new BinaryWriter(bytes, Encoding.UTF8, leaveOpen: true)) { writer.Write((ushort)0x1234); writer.Write(0x01020304); }
Console.WriteLine(Convert.ToHexString(bytes.ToArray())); bytes.Position = 0;
using (var reader = new BinaryReader(bytes, Encoding.UTF8, leaveOpen: true)) { Console.WriteLine(reader.ReadUInt16()); Console.WriteLine(reader.ReadInt32()); }
byte[] networkOrder = new byte[2]; BinaryPrimitives.WriteUInt16BigEndian(networkOrder, 0x1234); Console.WriteLine(Convert.ToHexString(networkOrder));
Microsoft documentation ↗
I/O & HTTP

The wrapper needs an ownership policy

leaveOpen determines whether disposing a wrapper also closes its underlying stream.

Read explanation & example

The first text writer leaves the MemoryStream open and flushes its buffered characters when disposed. The caller can then rewind and inspect the encoded bytes. A later reader is constructed with leaveOpen false, so disposing it closes the underlying stream too. The printed capability changes describe this explicit ownership transition. Keeping a stream open does not itself flush an active writer, rewind a cursor, or make concurrent access safe. Prefer one documented owner for the shared resource and a clear end boundary for every wrapper.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

using var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, new UTF8Encoding(false), bufferSize: 128, leaveOpen: true)) { writer.Write("AB"); }
Console.WriteLine(stream.CanRead); stream.Position = 0; Console.WriteLine(stream.ReadByte()); stream.Position = 0;
using (var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 128, leaveOpen: false)) { Console.WriteLine(reader.ReadToEnd()); }
Console.WriteLine(stream.CanRead);
Microsoft documentation ↗
I/O & HTTP

A digest is not proof of a trusted sender

SHA-256 fingerprints bytes; authentication requires a different contract.

Read explanation & example

The sample hashes the known UTF-8 bytes for abc and compares that result with the digest of abd. SHA-256 returns a fixed-size digest, not encrypted data and not a password-verification construction. An attacker able to replace both content and its untrusted checksum can replace them consistently. Trusted digest distribution, HMAC or signatures solve different parts of an integrity and authenticity design. Browser support depends on the algorithm and runtime; current .NET cryptography guidance lists managed SHA-2 support for browser WebAssembly, and this course uses SHA256.HashData only.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

byte[] digest = SHA256.HashData(Encoding.UTF8.GetBytes("abc"));
Console.WriteLine(Convert.ToHexString(digest));
Console.WriteLine(digest.SequenceEqual(SHA256.HashData(Encoding.UTF8.GetBytes("abd"))));
Microsoft documentation ↗
I/O & HTTP

A successful HTTP status can have no representation

204 is successful, but the application must not demand a JSON body from it.

Read explanation & example

The injected handler produces a real HttpResponseMessage with status 204 and an empty body, then a separate 404 response. IsSuccessStatusCode recognizes the first as successful without claiming that it contains a model. The consumer explicitly handles no-content before body parsing and prints a diagnostic for the unsuccessful response. Transport completion, HTTP status, media type, JSON syntax and domain validity are separate contracts. The offline handler sends no public request; it makes those decisions reproducible without network availability.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

using var client = new HttpClient(new StatusHandler());
foreach (string path in new[] { "empty", "missing" }) {
    using var request = new HttpRequestMessage(HttpMethod.Get, "https://offline.invalid/" + path);
    using var response = await client.SendAsync(request, CancellationToken.None);
    Console.WriteLine(response.IsSuccessStatusCode);
    if (response.StatusCode == HttpStatusCode.NoContent) Console.WriteLine("no content");
    else Console.WriteLine("status:" + (int)response.StatusCode);
}
sealed class StatusHandler : HttpMessageHandler {
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token) { token.ThrowIfCancellationRequested(); return Task.FromResult(request.RequestUri!.AbsolutePath == "/empty" ? new HttpResponseMessage(HttpStatusCode.NoContent) { Content = new ByteArrayContent(Array.Empty<byte>()) } : new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("not here") }); }
}
Microsoft documentation ↗
I/O & HTTP

Count bytes while consuming the body

ResponseHeadersRead changes the completion boundary; the consumer still needs a byte policy.

Read explanation & example

The sample asks HttpClient to complete after response headers and then reads the body stream with its own four-byte accepted-output limit. A four-byte body succeeds; a larger body is rejected before the next write to the accumulator. Content-Length is not the sole enforcement mechanism, because a stream may have no trusted length and actual bytes still matter. This small example bounds its accumulated result, not every upstream allocation, decompression ratio, parser cost or elapsed time. The response remains alive until its content stream has been consumed and disposed.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

using var client = new HttpClient(new BodyHandler());
foreach (string path in new[] { "small", "large" }) {
    using var request = new HttpRequestMessage(HttpMethod.Get, "https://offline.invalid/" + path);
    using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); response.EnsureSuccessStatusCode();
    using Stream body = await response.Content.ReadAsStreamAsync(CancellationToken.None);
    try { Console.WriteLine(Encoding.UTF8.GetString(await ReadBounded(body, 4, CancellationToken.None))); } catch (InvalidDataException) { Console.WriteLine("limit exceeded"); }
}
static async Task<byte[]> ReadBounded(Stream input, int maximum, CancellationToken token) {
    if (maximum < 0) throw new ArgumentOutOfRangeException(nameof(maximum)); using var result = new MemoryStream(); byte[] buffer = new byte[4]; int count;
    while ((count = await input.ReadAsync(buffer.AsMemory(), token)) != 0) { if (result.Length + count > maximum) throw new InvalidDataException("Body budget"); result.Write(buffer, 0, count); }
    return result.ToArray();
}
sealed class BodyHandler : HttpMessageHandler {
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token) { token.ThrowIfCancellationRequested(); string body = request.RequestUri!.AbsolutePath == "/small" ? "rune" : "oversize"; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(body), writable: false)) }); }
}
Microsoft documentation ↗
I/O & HTTP

A path expression is not a filesystem operation

Path.Combine composes local path text; existence and permission require a real filesystem.

Read explanation & example

The sample combines two relative path components and reports the filename, extension and rootedness without touching disk. It avoids printing a platform-specific separator so its output is deterministic in the embedded runtime. A path string does not create a directory, reserve a filename, establish read permission or prove that another process cannot change an entry. Rootedness, case behavior, invalid characters and symbolic links depend on the actual platform and APIs. Treat later desktop file experiments as a separate environment exercise with a user-selected scratch directory and explicit cleanup.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

string path = Path.Combine("data", "rune.json"); Console.WriteLine(Path.GetFileName(path)); Console.WriteLine(Path.GetExtension(path)); Console.WriteLine(Path.IsPathRooted(path));
Microsoft documentation ↗
Unity

A ray query has an eligibility policy

A mask, trigger rule and range filter answer different questions.

Read explanation & example

A Unity ray query receives several independent policy inputs: the geometric ray, a range, an eligible layer mask and a trigger rule. Excluding a collider from the query makes it absent from that observation; it does not make it a blocker that somehow remains visible to the same filtered query. An interaction system should include blocking geometry and then inspect the nearest result for the intended gameplay role. The portable function below checks only mask membership and a supplied trigger flag, including bounds on the supplied layer index. Its two outputs illustrate a policy distinction without calling Physics.Raycast, creating a collider or claiming that a wall was intersected. A real Unity scene still needs a correctly constructed ray, enabled colliders, appropriate synchronization and explicit integration evidence.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

bool Eligible(int layer, bool trigger, int mask, bool includeTriggers) => layer >= 0 && layer < 32 && (mask & (1 << layer)) != 0 && (includeTriggers || !trigger);
Console.WriteLine(Eligible(8, false, 1 << 8, false));
Console.WriteLine(Eligible(8, true, 1 << 8, false));
Console.WriteLine(Eligible(40, false, 1 << 8, false));
Unity documentation ↗
Unity

Coordinate-space names belong in a contract

A world offset and a parent-relative offset are not interchangeable.

Read explanation & example

Transform positions and orientations are meaningful only with their coordinate spaces. A world-space follow offset stays aligned to world axes, while an actor-relative offset rotates with the actor’s frame. Transform.forward is a world direction derived from the object’s local positive Z axis, and localRotation is relative to the parent. Naming those spaces in fields and APIs prevents a seemingly harmless refactor from changing gameplay meaning. The console practice adds a supplied world offset to a supplied position and prints the resulting desired point. It performs neither a parent transform nor a quaternion rotation. A later actor-relative implementation needs its own conversion and boundary checks, then actual Editor verification. Integer arithmetic here is an intentionally small model rather than a replacement for Unity’s transform system.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

var target = new Point(4, 1, -2);
var worldOffset = new Point(0, 3, -6);
var desired = new Point(target.X + worldOffset.X, target.Y + worldOffset.Y, target.Z + worldOffset.Z);
Console.WriteLine($"{desired.X},{desired.Y},{desired.Z}");
record Point(int X, int Y, int Z);
Unity documentation ↗
Unity

A visual-element reference belongs to one document tree

Rebind after independent UIDocument replacement rather than retaining stale elements.

Read explanation & example

A UI Toolkit controller queries elements from one UIDocument root. A document rebuild can replace that tree, leaving an old cached Button unrelated to the current visible control. Pairing OnEnable and OnDisable handles the normal controller activation lifetime, while an independently changed document needs an explicit rebind after its new root is ready. Guard a disabled document, a null root and missing element names separately from input-routing failures. The plain C# counter below acquires and releases an event subscription idempotently. It can verify that repeated acquisition does not duplicate this handler and that release prevents further count changes. It has no visual tree, pointer route or UIDocument and therefore cannot verify a real UI Toolkit reconstruction. Use the model check and Editor interaction as separate evidence.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

var source = new Signal(); var listener = new Listener(source);
listener.Attach(); listener.Attach(); source.Raise();
Console.WriteLine(listener.Count);
listener.Detach(); source.Raise();
Console.WriteLine(listener.Count);
sealed class Signal { public event Action? Raised; public void Raise() => Raised?.Invoke(); }
sealed class Listener
{
    private readonly Signal source; private bool attached;
    public int Count { get; private set; }
    public Listener(Signal source) { this.source = source; }
    public void Attach() { if (attached) return; attached = true; source.Raised += Increment; }
    public void Detach() { if (!attached) return; source.Raised -= Increment; attached = false; }
    private void Increment() { Count++; }
}
Unity documentation ↗
Unity

Requested is not arrived

Treat pending, partial and absent-path observations as distinct states.

Read explanation & example

SetDestination returning true says that the NavMeshAgent accepted a destination request. A route can still be pending, partial, invalid or absent, and remainingDistance can be unknown. A conservative arrival rule should first establish the observations it needs, then check finite distance and movement state. No active path is not automatically success: it can arise at an initially satisfied target or after an external path reset, which need a separately defined command policy. The portable predicate uses supplied booleans and numeric observations. It rejects pending and incomplete paths before considering distance, and its false, false, true output verifies only that evidence ordering. It does not create a NavMeshAgent or prove reachability. Actual navigation still requires compatible baked data, binding, package setup and a real engine run.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

bool ObservedArrival(bool pending, bool complete, bool hasPath, double distance, double stopping, double speedSquared) => !pending && complete && hasPath && double.IsFinite(distance) && distance >= 0 && stopping >= 0 && double.IsFinite(stopping) && speedSquared >= 0 && double.IsFinite(speedSquared) && distance <= stopping && speedSquared <= 0.01;
Console.WriteLine(ObservedArrival(true, true, true, 0, 0.2, 0));
Console.WriteLine(ObservedArrival(false, true, false, 0, 0.2, 0));
Console.WriteLine(ObservedArrival(false, true, true, 0.1, 0.2, 0));
Unity documentation ↗
Current C# & .NET

A target type is not an ownership policy

Separate a collection expression from assigning a view of an existing collection.

Read explanation & example

Collection expressions need context for their target type, but that type name does not answer every ownership question. An IReadOnlyList can be constructed independently from bracketed elements, or it can refer to an existing mutable List. In the second case, mutations through the owner remain visible through the read-only interface. A spread-built integer array is a separate materialized container, so a later List append does not extend it. The sample deliberately places those two relationships beside each other. Predict the read-only count from its shared owner and the snapshot length from its earlier construction. Avoid casting an interface result to a guessed implementation to establish immutability. If independent element objects are required, define a copying policy for those objects as well; creating another container copies reference elements rather than recursively cloning their graphs.

using System;
using System.Collections.Generic;
List<int> owner = [2, 4];
IReadOnlyList<int> readOnly = owner;
int[] snapshot = [.. owner];
owner.Add(6);
Console.WriteLine($"view:{readOnly.Count}");
Console.WriteLine($"snapshot:{snapshot.Length}");
Microsoft documentation ↗
Current C# & .NET

Avoid two competing primary-constructor states

An initialized property and a captured parameter can diverge.

Read explanation & example

A primary-constructor parameter can initialize a declared member and can also be retained for later instance behavior. If code uses both forms, it may create two representations of what readers think is one value. The sample initializes Initial from the input, then exposes Current through the captured parameter. Updating that parameter changes Current while Initial remains the construction snapshot. This is legal and sometimes deliberate, but it should not happen accidentally during a conversion from a conventional constructor. Ordinary class primary constructors do not automatically generate public properties, and parameters used from later members are not implicitly readonly. Review every parameter reference and decide whether the intended model needs initial state, current state, or one explicit field. The two observed values are evidence of the source contract, not a measurement of generated storage layout.

using System;
var reading = new Reading(2);
reading.Set(5);
Console.WriteLine($"initial:{reading.Initial}");
Console.WriteLine($"current:{reading.Current}");
sealed class Reading(int value)
{
    public int Initial { get; } = value;
    public int Current => value;
    public void Set(int next) => value = next;
}
Microsoft documentation ↗
Current C# & .NET

Aliases do not create nominal safety

Compatible tuple aliases can still be assigned to one another.

Read explanation & example

An alias provides another source-level name for a type; it does not create a distinct runtime or nominal domain type. Position and Extent in the sample both describe two integer tuple elements, so assignment remains compatible despite their different conceptual meanings and element names. The receiving variable exposes its own tuple element names, producing width two and height three from the original position. This can be convenient for local readability, but it cannot prevent mixing coordinates with dimensions in a public API. If that distinction is important, declare real named types and implement any required validation. The same limitation applies to aliases for arrays: renaming a reference type does not copy its contents or change its mutability. Keep naming convenience, type identity, and ownership as separate design decisions when adopting C# 12 aliases.

using System;
using Position = (int X, int Y);
using Extent = (int Width, int Height);
Position position = (2, 3);
Extent extent = position;
Console.WriteLine($"width:{extent.Width}");
Console.WriteLine($"height:{extent.Height}");
Microsoft documentation ↗
Current C# & .NET

Frozen associations can contain mutable values

Freezing a map does not clone the objects stored in its values.

Read explanation & example

A FrozenDictionary fixes its key-to-value associations while retaining the semantics of each stored value. In this example, the value is a mutable List, and the builder and frozen map initially refer to that same List object. Appending through the retained object therefore changes the contents obtained from a frozen lookup. Adding another key to the builder does not add that association to the frozen map. These two observations distinguish container immutability from deep object-graph immutability. Prefer immutable value models or an explicit copying policy when publishing configuration that must remain stable. Keep the comparer and duplicate-key policy deliberate as well. Freezing is intended for suitable read-heavy workloads, and its construction cost means that rebuilding it for each request should be justified by measurements rather than by the attractiveness of an immutable-sounding collection name.

using System;
using System.Collections.Generic;
using System.Collections.Frozen;
var items = new List<int> { 1, 2 };
var source = new Dictionary<string, List<int>> { ["a"] = items };
var frozen = source.ToFrozenDictionary(StringComparer.Ordinal);
items.Add(3);
source["b"] = new List<int>();
Console.WriteLine($"items:{frozen["a"].Count}");
Console.WriteLine($"keys:{frozen.Count}");
Console.WriteLine($"new-key:{frozen.ContainsKey("b")}");
Microsoft documentation ↗
Current C# & .NET

Character indices use UTF-16 units

A supplementary Unicode scalar occupies two char positions before the separator.

Read explanation & example

SearchValues of char finds matching UTF-16 code units in a .NET character span. The supplementary crab symbol in the sample is represented by a surrogate pair, so the semicolon after it is at index two rather than index one. Slicing before that index preserves both code units and produces the complete symbol. This does not mean every user-visible character has two units, nor that UTF-16 indices can be used as UTF-8 byte offsets or grapheme positions. Name index units explicitly at parsing boundaries. An ASCII separator set remains a useful delimiter policy, while normalization and user-perceived text segmentation require separate decisions and APIs. Always handle the negative not-found sentinel before slicing a general input. Reusing a search set changes neither the representation's indexing rules nor the caller's responsibility to return valid owned text.

using System;
using System.Buffers;
SearchValues<char> separators = SearchValues.Create(";");
ReadOnlySpan<char> text = "🦀;x";
int index = text.IndexOfAny(separators);
Console.WriteLine($"index:{index}");
Console.WriteLine("prefix:" + text[..index].ToString());
Microsoft documentation ↗
Current C# & .NET

Params collection call forms have one contract

Expanded arguments, empty input, and an existing array all reach the declared collection parameter.

Read explanation & example

C# 13 allows a params ReadOnlySpan parameter, so a caller can provide separate elements without requiring an array parameter declaration. The sample observes only the number of supplied values. Expanded arguments contribute three values, an omitted argument list contributes none, and an existing two-element array supplies two. This is genuine C# 13 source in the .NET 9 profile. It does not establish which storage strategy the compiler chooses for every call, and the read-only span does not become an independently owned immutable collection. Consume the view during the call and return ordinary results when no retention is required. If an API must keep input for later asynchronous work, choose a suitable ownership and lifetime contract instead of capturing the span. Keep overload-resolution changes and runtime allocation measurements separate from these simple deterministic count observations.

using System;
int Count(params ReadOnlySpan<int> values) => values.Length;
int[] existing = [7, 8];
Console.WriteLine($"expanded:{Count(1, 2, 3)}");
Console.WriteLine($"empty:{Count()}");
Console.WriteLine($"array:{Count(existing)}");
Microsoft documentation ↗
Architecture & Design

Runtime dispatch does not reverse source ownership

An outer implementation can be called through an inner-owned contract.

Read explanation & example

A source dependency describes which declaration the caller must reference to compile. Runtime dispatch describes which supplied object actually executes a method. Dependency inversion lets these directions differ: a use case knows a port, while an outer adapter implements that port and is selected by the host. The sample demonstrates genuine interface dispatch with a deterministic response, but all declarations are in one portable compilation. It therefore does not prove assembly isolation. Use the architecture course's external projects and their actual ProjectReference checker for that evidence. Avoid placing an infrastructure-specific type in an inner signature and then claiming an interface elsewhere fixed the dependency. Contract ownership, composition, and runtime behavior should each be visible. A dependency-injection container is optional; explicit constructor composition can establish the same small collaboration without a framework package.

using System;
var useCase = new ReadName(new FixedName());
Console.WriteLine(useCase.Execute());
interface INameSource { string Read(); }
sealed class ReadName(INameSource source) { public string Execute() => source.Read(); }
sealed class FixedName : INameSource { public string Read() => "rune"; }
Microsoft documentation ↗
Architecture & Design

Parsing policy is not the domain invariant

Keep malformed representation separate from a typed but rejected quantity.

Read explanation & example

An inbound adapter converts representation into an application request, while the domain decides whether a typed value is meaningful. A successfully parsed zero is different from text that cannot be converted to an integer. The sample uses an explicit invariant parsing policy and returns different stable labels for those two stages. A positive accepted quantity then reaches the domain rule. This is a real local boundary calculation, not an HTTP controller or a framework validation pipeline. Keep unexpected infrastructure exceptions out of the ordinary invalid-input category. Tests should independently challenge parsing grammar, domain boundaries, and presentation mapping. If a transport later adds status codes or versioned payloads, verify that actual adapter separately; a green BCL-only comparison cannot prove every external representation maps correctly or that a public result type prevents all invalid combinations.

using System;
using System.Globalization;
string Evaluate(string text)
{
    if (!int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out int units)) return "text";
    return units >= 1 && units <= 3 ? "accepted" : "quantity";
}
Console.WriteLine(Evaluate("x"));
Console.WriteLine(Evaluate("0"));
Console.WriteLine(Evaluate("2"));
Microsoft documentation ↗
Architecture & Design

A read model need not expose mutable write state

Return a deliberate immutable view and test that later changes do not rewrite it.

Read explanation & example

Separating a write operation from a read representation does not require a mediator library or separate database. A query can project current domain state into a small immutable value for readers. The sample captures a CountView, changes the domain object, and shows that the captured value remains the earlier observation. That snapshot is different from exposing the mutable Counter directly and calling it a read model. The example uses one process and one state object; it does not implement an asynchronous projection, event replay, or eventual-consistency guarantee. If a production query model is updated separately, freshness and duplicate delivery become explicit contracts needing their own evidence. Keep domain mutation methods out of read DTOs and test ownership assumptions rather than inferring immutability solely from a property or class name.

using System;
var state = new Counter();
state.Add(2);
var view = new CountView(state.Value);
state.Add(3);
Console.WriteLine($"view:{view.Value}");
Console.WriteLine($"current:{state.Value}");
sealed class Counter { public int Value { get; private set; } public void Add(int units) => Value = checked(Value + units); }
record CountView(int Value);
Microsoft documentation ↗
Architecture & Design

A recorded event is not durable delivery

Separate local publication intent from transaction and dispatcher guarantees.

Read explanation & example

An ordinary list can record which event an application intends to publish, but it cannot establish durability, atomic database commit, or broker delivery. The sample records one immutable event value and prints its identity and units. Those observations are useful for a local orchestration contract, provided they are labeled honestly. A production transactional outbox persists business state and message intent in an appropriate shared storage transaction, then a dispatcher and consumer handle retries and duplicate delivery according to their own contracts. Two consecutive list writes do not provide that mechanism. Likewise, a method named Commit cannot manufacture atomicity for an implementation that has none. Keep event creation, persistence, publication, and consumption as separately owned responsibilities, and verify real provider behavior before claiming that accepted changes cannot lose or duplicate external effects.

using System;
using System.Collections.Generic;
var pending = new List<Accepted> { new("A1", 2) };
Console.WriteLine($"pending:{pending.Count}");
Console.WriteLine($"event:{pending[0].Id}:{pending[0].Units}");
record Accepted(string Id, int Units);
Microsoft documentation ↗
Architecture & Design

A graph fixture is not a project scan

Test the rule with declared edges, then inspect the real artifact separately.

Read explanation & example

A dependency-rule function can be tested with small valid and invalid edge fixtures. That establishes the rule's behavior on supplied data, not the actual assembly graph of a running application. The sample deliberately labels its two observations as declared relationships. The complete architecture labs supply a separate .NET tool that reads physical project-reference XML and rejects forbidden directions. Its scope is the explicit unconditional project items used by those labs, not all evaluated MSBuild imports, packages, runtime reflection, or service calls. Modern .NET references can also be transitive, so an absent direct item does not prove arbitrary type invisibility. Retain negative fixtures and nonzero failure signals for real checks. A tool that always reports success can look reassuring when it is only ever run against a valid example.

using System;
bool Allowed(string from, string to) => from == "Application" && to == "Domain";
Console.WriteLine($"declared-valid:{Allowed("Application", "Domain")}");
Console.WriteLine($"declared-invalid:{Allowed("Domain", "Infrastructure")}");
Microsoft documentation ↗
Architecture & Design

Instance lifetime and state authority differ

A local object graph cannot establish a cluster-wide quota.

Read explanation & example

A singleton is scoped to the container or ownership arrangement that creates it; it is not automatically unique across processes, deployments, or a cluster. The sample deliberately creates two independent stores in one process and observes their separate values. It launches no servers and is not a load test. Thread safety inside one instance, durability across restart, and coordination among instances are different contracts. A global quota may need an authoritative shared atomic operation, while an approximate metric or disposable cache may intentionally remain local. Moving every value to a remote store also introduces latency, availability, and consistency decisions. State the required scope before choosing a lifetime or storage mechanism, then test real competing clients and failures against the actual adapter when the requirement extends beyond the bounded local ownership shown here.

using System;
var first = new LocalState();
var second = new LocalState();
first.Add(); first.Add();
Console.WriteLine($"first:{first.Value}");
Console.WriteLine($"second:{second.Value}");
sealed class LocalState { public int Value { get; private set; } public void Add() => Value = checked(Value + 1); }
Microsoft documentation ↗
Load & distributed systems

A percentile needs a population and estimator

Nearest-rank values over a fixed sample are not universal latency targets.

Read explanation & example

The companion computes nearest-rank percentiles for five specified observations and a units-explicit Little’s Law calculation. A tiny sample makes tail percentiles coarse, and per-attempt latency differs from end-to-end logical-operation latency when retries occur. Average work, rate and time must refer to the same stable system boundary. Do not infer sustainable capacity from a requested arrival rate or average together percentiles from replicas. The real driver reports counts with its measured attempt durations so the observed population remains visible.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

int[] sorted = new[] { 10, 20, 30, 40, 100 }; int P(double p) => sorted[(int)Math.Ceiling(p * sorted.Length) - 1];
Console.WriteLine("p50:" + P(.5)); Console.WriteLine("p95:" + P(.95)); Console.WriteLine("rate:" + (4m / .2m).ToString("0.00", CultureInfo.InvariantCulture));
Microsoft documentation ↗
Load & distributed systems

Queue capacity and active capacity are separate

Bound waiting work without hiding excess demand.

Read explanation & example

The ordinary queue model accepts only its stated waiting capacity. Real worker concurrency adds another budget, so two active workers and three waiting slots can own five operations. The external Channel experiment observes that exact controlled snapshot with real HTTP requests before rejecting a sixth admission. An in-memory queue is not durable storage, and a rejection is not permission for unlimited retries. Backpressure, caller deadlines and completion ownership should be designed together.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

var waiting = new Queue<string>(); const int capacity = 2;
foreach (string item in new[] { "A", "B", "C" }) { bool accepted = waiting.Count < capacity; if (accepted) waiting.Enqueue(item); Console.WriteLine(accepted ? "accepted" : "rejected"); }
Console.WriteLine("next:" + waiting.Dequeue()); Console.WriteLine("queued:" + waiting.Count);
Microsoft documentation ↗
Load & distributed systems

Local pools multiply without expanding a shared dependency

Replica-local admission and deployment-wide resource budgets differ.

Read explanation & example

The model takes the minimum of local worker and pool limits, then counts two independent local budgets. This arithmetic does not predict throughput or make an external database twice as capable. More processes can add pressure to the same downstream system even while each process respects its own semaphore. The real testbed records worker and teaching-pool occupancy separately. Keep per-request, per-process, per-dependency and deployment-wide capacities in their own units.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

int workers = 3, pool = 2; int local = Math.Min(workers, pool); Console.WriteLine("local slots:" + local); Console.WriteLine("two local budgets:" + (2 * local)); Console.WriteLine("shared limit still:" + pool);
Microsoft documentation ↗
Load & distributed systems

Attempts can amplify logical demand

Jitter changes schedules without granting permission to repeat a write.

Read explanation & example

Five logical operations with three attempts each can consume fifteen attempts, and stacked retry layers can multiply that bound. The fixed companion offsets are a deterministic teaching device rather than a recommended production seed. A real retry design needs attempt and time budgets, failure classification, server guidance and an idempotency contract for effects. The external driver only retries a controlled GET and exposes attempted versus scheduled counts, preventing per-attempt observations from silently becoming logical-operation claims.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

int operations = 5, maxAttempts = 3; Console.WriteLine("maximum attempts:" + operations * maxAttempts); int[] offsets = { 3, 7, 2 };
Console.WriteLine("teaching waits:" + string.Join(",", Enumerable.Range(0, 3).Select(i => 20 * (1 << i) + offsets[i])));
Microsoft documentation ↗
Load & distributed systems

A recorded key has a transaction boundary

A local ledger does not prove exactly-once external effects.

Read explanation & example

The portable dictionary verifies created, replay and conflict policy in one owner. The external two-process SQLite experiment is separate evidence of local-file uniqueness and transactional coordination. A key must identify the intended operation and reject incompatible reuse; a transaction that records a row cannot atomically include an unrelated payment service by assertion. Retention, failures, concurrency, process crashes and database availability require their own rules. Preserve those limits when using idempotency terminology.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

var ledger = new Dictionary<string, int>(StringComparer.Ordinal);
string Record(string key, int amount) { if (ledger.TryGetValue(key, out int stored)) return stored == amount ? "replay" : "conflict"; ledger.Add(key, amount); return "created"; }
Console.WriteLine(Record("demo", 25)); Console.WriteLine(Record("demo", 25)); Console.WriteLine(Record("demo", 30));
Microsoft documentation ↗
Load & distributed systems

Aggregate the observations you actually collected

Mean of replica means can differ from the mean across requests.

Read explanation & example

Two requests at ten units and eight requests at one hundred units yield a request-weighted mean of eighty-two, while averaging the two local means yields fifty-five. They describe different populations. The same care is needed before diagnosing slow single-instance work: CPU use, ThreadPool queues, active threads and latency are different observations. A deliberately blocking endpoint can demonstrate a code path without a tiny load proving actual starvation. Use bounded experiments and supporting runtime evidence rather than declaring a universal performance result.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

int fastCount = 2, slowCount = 8; decimal global = (fastCount * 10m + slowCount * 100m) / (fastCount + slowCount);
Console.WriteLine("request mean:" + global.ToString("0.00", CultureInfo.InvariantCulture)); Console.WriteLine("mean of means:" + ((10m + 100m) / 2).ToString("0.00", CultureInfo.InvariantCulture));
Microsoft documentation ↗
ASP.NET Core

Binding is not a domain range check

A parsed quantity can still violate the application contract.

Read explanation & example

The console checks a nullable quantity after choosing an explicit default. It demonstrates only the domain rule. In a real endpoint, route matching, scalar conversion and application validation have different responsibilities and may produce different status outcomes. An integer route constraint can prevent a path from matching; a malformed query can fail binding; a bound zero can still fail the business range. Test each boundary independently in the external server rather than assigning every rejected input to one generic category.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

foreach (int? amount in new int?[] { null, 3, 0 }) Console.WriteLine(Choose(amount)?.ToString(CultureInfo.InvariantCulture) ?? "rejected");
static int? Choose(int? amount) { int value = amount ?? 1; return value is >= 1 and <= 10 ? value : null; }
Microsoft documentation ↗
ASP.NET Core

Result categories need an explicit adapter

Domain absence and invalid input are separate from a successful lookup.

Read explanation & example

The small dictionary lookup distinguishes an invalid identifier, an existing item and a missing item without borrowing HTTP types. An ASP.NET adapter can map those categories to genuine typed results and ProblemDetails. The mapping is part of the API contract, not an automatic consequence of calling a method minimal. A typed union helps expose supported alternatives, but response metadata and actual behavior should still be compared through real requests. Do not return 200 merely because every branch successfully executed some C# code.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var catalog = new Dictionary<int, string> { [1] = "rune" };
foreach (int id in new[] { 0, 1, 9 }) Console.WriteLine(id <= 0 ? "invalid" : catalog.TryGetValue(id, out string? name) ? "found:" + name : "missing");
Microsoft documentation ↗
ASP.NET Core

Scope identity belongs to an owner

A singleton should not silently capture a request-owned dependency.

Read explanation & example

The portable objects demonstrate identity within one owner and independence across two owners. They do not constitute a DI container. In the real application, the service provider implements singleton, scoped and transient lifetimes and owns appropriate disposal boundaries. Registering a service with a lifetime does not make its mutable state thread-safe. Resolving a scoped dependency from the root or retaining it in a singleton changes its intended ownership and can fail scope validation. Use a real scope for work that must outlive an individual request.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var first = new Owner(); object borrowed = first.Value; Console.WriteLine(ReferenceEquals(first.Value, borrowed));
var second = new Owner(); Console.WriteLine(ReferenceEquals(first.Value, second.Value));
sealed class Owner { public object Value { get; } = new object(); }
Microsoft documentation ↗
ASP.NET Core

Request cancellation must reach cooperative work

Stopping an awaited operation does not reverse completed side effects.

Read explanation & example

The pre-canceled console operation checks its owning token before completing. ASP.NET can supply RequestAborted through a bound CancellationToken, but downstream APIs only cooperate when the token reaches operations that honor it. A disconnect test in the real server is different evidence from a console pre-cancellation check. Neither proves that a remote action was rolled back. Keep caller cancellation, application timeouts, response failures and business reconciliation distinct, and keep resources alive until their operations have actually finished.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using var lifetime = new CancellationTokenSource(); lifetime.Cancel(); int completed = 0;
try { await Complete(lifetime.Token); completed++; } catch (OperationCanceledException) { Console.WriteLine("cancelled"); }
Console.WriteLine("completed:" + completed);
static Task Complete(CancellationToken token) { token.ThrowIfCancellationRequested(); return Task.CompletedTask; }
Microsoft documentation ↗
ASP.NET Core

Public reuse needs a complete variation key

Private observations should not accidentally enter a public representation cache.

Read explanation & example

The portable dictionary memoizes one public topic key and bypasses reuse for private observations. It demonstrates a domain reuse rule, not an HTTP cache. ASP.NET output caching adds its own eligibility and variation policies, and a cache key must account for every dimension that changes the reusable representation. Keep authenticated or personalized data outside a public teaching policy, preserve framework safeguards and verify actual response behavior. A Cache-Control header and authorization policy answer different questions; neither substitutes for the other.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var cache = new Dictionary<string, int>(StringComparer.Ordinal); int generation = 0;
int Public(string topic) { if (!cache.TryGetValue(topic, out int value)) cache[topic] = value = ++generation; return value; }
Console.WriteLine(Public("csharp")); Console.WriteLine(Public("csharp")); Console.WriteLine(++generation); Console.WriteLine(++generation);
Microsoft documentation ↗
ASP.NET Core

An observation is not an exporter

Bound operation dimensions before instrumenting the application.

Read explanation & example

The console aggregates two known quote amounts as domain data. In the external project, real ActivitySource and Meter instruments emit observations to explicitly installed local listeners. Without a configured listener an ActivitySource may return no activity, and a measurement observed locally is not proof of delivery to a collector. Choose bounded operation labels instead of unbounded user identifiers as metric dimensions. Logs, traces and metrics provide related but different evidence, with independent configuration, ownership and export lifetimes.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var amounts = new[] { 12.5m, 12.5m }; Console.WriteLine("quote:" + amounts.Length); Console.WriteLine("amount:" + amounts.Sum().ToString("0.00", CultureInfo.InvariantCulture));
Microsoft documentation ↗
EF Core

A context has an operation owner

Disposal and an externally owned connection answer different lifetime questions.

Read explanation & example

A DbContext should have a clear short-lived operation owner, and outstanding calls on the same instance must be awaited sequentially. Contexts are not thread-safe. An externally supplied open connection has a separate owner; private in-memory SQLite data survives only while its owning connection remains open. A factory-created context still needs disposal, even if pooling returns its instance for a later lease. The BCL snippet below verifies only that a disposable lease records one release despite repeated Dispose calls. It contains no EF context or database connection. Use that narrow ownership check alongside a genuine external relational test rather than promoting it into proof of context reset, SQL execution or storage persistence. A production adapter must also define what ends a failed unit and whether a context remains usable after a programming error.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

var lease = new Lease(); lease.Dispose(); lease.Dispose(); Console.WriteLine(lease.Releases);
sealed class Lease : IDisposable { public int Releases { get; private set; } public void Dispose() { if (Releases == 0) Releases = 1; } }
Microsoft documentation ↗
EF Core

Materialization is an execution boundary

A supplied Enumerable pipeline does not prove provider translation.

Read explanation & example

An EF IQueryable contains an expression that its provider translates when execution is requested. An Enumerable pipeline over an array reads already supplied application values. Both can defer work until enumeration, but they are not interchangeable evidence: a successful array filter cannot establish SQL translation, selected columns or an index plan. Scalar projections also differ from projections that contain actual entity instances and may therefore track them. The .NET9 code below observes only a deferred Enumerable selector. Its visit count is zero before materialization and three afterward. That is a useful BCL execution-order contract, not a query provider or EF emulator. Real EF checks should inspect the translated query where helpful and actually materialize independently expected results against the intended relational provider.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

int visits = 0;
var values = new[] { 1, 2, 3 }.Select(x => { visits++; return x * 2; });
Console.WriteLine(visits); _ = values.ToArray(); Console.WriteLine(visits);
Microsoft documentation ↗
EF Core

A concurrency token needs original evidence

Configure the property and advance application-managed values on relevant writes.

Read explanation & example

EF optimistic concurrency protects an update or delete by including the original token value read by the context. A stored token that no longer matches can produce an affected-row conflict. SQLite lacks SQL Server’s automatic rowversion behavior, so an application-managed token must be configured and advanced deliberately. A property named Version is not enough, and an insert’s unique violation is a different failure mechanism. The portable sample compares supplied versions and preserves the proposed edit as ordinary data. It cannot atomically compare and update storage or prove that EF generated a token predicate. Use the actual external SQLite lesson for that evidence. A real conflict-resolution policy must decide how to reload, reject or reconcile values rather than blindly overwrite the winner or repeat a stale update.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

var proposed = new Proposal(1, "user edit"); int storedVersion = 2;
Console.WriteLine(proposed.ExpectedVersion == storedVersion ? "can propose" : "conflict");
Console.WriteLine(proposed.Text);
record Proposal(int ExpectedVersion, string Text);
Microsoft documentation ↗
EF Core

Named filters are scoped read defaults

Trusted tenant selection and permitted bypasses require application policy.

Read explanation & example

EF10 named filters can keep a tenant rule active while selectively disabling a soft-delete rule. The context should receive its tenant from a trusted operation owner, and query filters must not be confused with authentication or a complete authorization boundary. They do not automatically validate TenantId on added entities, prevent raw SQL access or make IgnoreQueryFilters safe for every caller. Pooling adds per-lease state concerns that need explicit handling. The BCL code filters supplied records using an already chosen tenant string. Its count proves only that predicate on that array. It does not establish EF translation or production isolation. Keep normal read scope, administrative bypass, write ownership and authenticated tenant resolution as distinct contracts with actual provider integration checks where they matter.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

var supplied = new[] { new Row("A", false), new Row("A", true), new Row("B", false) };
Console.WriteLine(supplied.Count(x => x.Tenant == "A" && !x.Deleted));
record Row(string Tenant, bool Deleted);
Microsoft documentation ↗
EF Core

A cursor needs a complete unique ordering

Tie breakers belong in both the ordering and exclusive predicate.

Read explanation & example

A keyset cursor describes a boundary in an explicitly ordered result. If several rows share SortKey, Id can provide the unique tie breaker, and both values must participate in the next-page predicate. A keyset does not freeze membership across requests, authorize a tenant scope or guarantee an index plan. Mutable sort keys and backward paging need their own policies. The .NET9 snippet tests an exclusive comparison against supplied integer pairs only. It does not issue SQL or prove SQLite uses an index. Pair this specification with a real provider query and exact expected row identities. Validate cursor shape and bounded page size at the application boundary, keeping encoded cursor text separate from any authority to read data.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

var keys = new[] { (Sort: 10, Id: 2), (Sort: 10, Id: 3), (Sort: 20, Id: 1) };
Console.WriteLine(string.Join(",", keys.Where(x => x.Sort > 10 || x.Sort == 10 && x.Id > 2).OrderBy(x => x.Sort).ThenBy(x => x.Id).Select(x => x.Id)));
Microsoft documentation ↗
EF Core

Duplicate identity and stale versions differ

Do not flatten unrelated storage failures into one convenient conflict label.

Read explanation & example

A unique constraint rejects duplicate stored identity according to the database’s configured index and collation. An optimistic-concurrency exception instead reports an update or delete whose original evidence no longer matches. Input validation, provider availability and invalid schema can cause other failures again. A useful adapter translates only failures it can actually identify and ends or repairs a failed tracking unit deliberately. The portable code below classifies supplied application facts with an explicit precedence. It is not catching fabricated SQLite exceptions or pretending to execute a constraint. Its duplicate and stale outputs specify vocabulary only. The real EF lessons use actual relational failures and fresh stored-state verification, while production adapters need provider-specific classification and reconciliation rather than localized exception-message matching.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

string Decide(bool duplicateIdentity, bool staleVersion) => duplicateIdentity ? "duplicate" : staleVersion ? "stale" : "accepted";
Console.WriteLine(Decide(true, false)); Console.WriteLine(Decide(false, true));
Microsoft documentation ↗
CLR & Concurrency

Read assignability from target to source

Type identity, inheritance, and open generic definitions are different questions.

Read explanation & example

IsAssignableFrom asks whether the receiving Type can accept a value whose type is the supplied argument. Reversing those roles changes the question. The sample uses a base interface and a concrete implementation, so the interface accepts the concrete type while the converse is false. This is actual metadata inspection, not a dynamic cast or object conversion. An open generic definition is also not a wildcard assignability check for every closed construction; inspect definitions and arguments deliberately when that is the requirement. Keep runtime identity separate from simple type names, which can collide across namespaces and assemblies. In trimmed or NativeAOT deployments, metadata availability and instantiation reachability need their own plan. A successful desktop Type query does not prove an arbitrary plugin can be loaded, constructed, or safely invoked in every supported runtime.

using System;
Console.WriteLine($"target:{typeof(INamed).IsAssignableFrom(typeof(Named))}");
Console.WriteLine($"reverse:{typeof(Named).IsAssignableFrom(typeof(INamed))}");
interface INamed { }
sealed class Named : INamed { }
Microsoft documentation ↗
CLR & Concurrency

An attribute is metadata until a consumer acts

Inspect declared arguments without manufacturing automatic enforcement.

Read explanation & example

An attribute can record useful intent without changing the annotated method's behavior. CustomAttributeData reads those recorded arguments without invoking the custom attribute constructor. The sample discovers a method's label and separately calls the method, which returns its ordinary value. Nothing enforces a permission, changes dispatch, or adds validation merely because a marker is present. A real framework can define such a policy, but its reader and execution path are additional code that must be understood and tested. Avoid instantiating arbitrary user-defined attributes when metadata inspection alone is sufficient, since constructors can have effects or fail. Also keep inherited attribute rules, allow-multiple rules, and metadata preservation explicit. This small program demonstrates a direct declared-method annotation and a bounded ordinary call, not a security mechanism or an unrestricted assembly scanner.

using System;
using System.Linq;
using System.Reflection;
var method = typeof(Rules).GetMethod(nameof(Rules.Value))!;
var data = CustomAttributeData.GetCustomAttributes(method).Single(item => item.AttributeType == typeof(LabelAttribute));
Console.WriteLine($"label:{data.ConstructorArguments[0].Value}");
Console.WriteLine($"value:{Rules.Value()}");
static class Rules { [Label("audit")] public static int Value() => 3; }
[AttributeUsage(AttributeTargets.Method)] sealed class LabelAttribute(string name) : Attribute { public string Name { get; } = name; }
Microsoft documentation ↗
CLR & Concurrency

An explicit expression input avoids a hidden ambient dependency

Parameterization is a dependency decision, not proof of provider translation.

Read explanation & example

An expression tree can make input dependencies explicit by representing a parameter instead of reading captured ambient storage. The sample constructs and genuinely interprets a one-parameter arithmetic tree, then supplies two different inputs. Its output follows those inputs directly. This differs from an expression that closes over a mutable local and observes later assignments when executed. The tree is still data and may be inspected before interpretation. It is not automatically a SQL query, a frozen value snapshot, or a promise that every provider supports its nodes. Compiling or interpreting a tree also does not establish identical allocation or performance to an ordinary delegate. When designing a translator, validate its supported node shapes and handle unsupported expressions explicitly. When designing a bounded local calculation, prefer visible inputs over hidden mutable ambient state where that clarifies ownership.

using System;
using System.Linq.Expressions;
Expression<Func<int, int>> tree = input => input + 2;
var apply = tree.Compile(preferInterpretation: true);
Console.WriteLine($"first:{apply(3)}");
Console.WriteLine($"second:{apply(5)}");
Microsoft documentation ↗
CLR & Concurrency

Value equality is not a CAS expected reference

A lookalike immutable record does not match a different published instance.

Read explanation & example

The reference-type CompareExchange overload matches object identity rather than calling a record's value-equality operator. A newly allocated record with the same fields therefore cannot stand in for the exact reference a reader previously observed. The sample makes that difference visible without creating OS threads. Its failed exchange returns the actual current reference and leaves the published value unchanged. A real retry loop should use that observation or read again, recompute a candidate from valid immutable state, and perform nonrepeatable effects only after successful publication. This does not supply database transactions or external exactly-once delivery. It also does not magically prevent ABA if a protocol permits an old reference to be published again. Define the state-transition and lifetime rules, not merely a value equality method, when reasoning about a lock-free algorithm.

using System;
using System.Threading;
State current = new(3);
State lookalike = new(3);
State returned = Interlocked.CompareExchange(ref current, new State(4), lookalike);
Console.WriteLine($"value-equal:{current == lookalike}");
Console.WriteLine($"returned-current:{ReferenceEquals(returned, current)}");
Console.WriteLine($"unchanged:{current.Count}");
sealed record State(int Count);
Microsoft documentation ↗
CLR & Concurrency

A permit needs one explicit release owner

A maximum detects some over-release; it does not identify the acquiring thread.

Read explanation & example

SemaphoreSlim does not associate a permit with the thread that acquired it. Correct code must record successful acquisition and release exactly what it owns. A configured maximum can reject an extra release, but that is not a substitute for an ownership design: a wrong caller may release a genuinely held permit without exceeding the maximum. The sample intentionally acquires once, releases once, then observes the explicit over-release exception on a second release. It does not create a worker thread or establish fairness. A lease object can detach its release responsibility atomically so repeated disposal becomes harmless. Cancellation before acquisition must not enter the release path, and cancellation of a caller's wait is separate from disposal of a shared synchronization primitive. Keep these lifetime decisions visible rather than putting an unconditional Release in every finally block.

using System;
using System.Threading;
using var gate = new SemaphoreSlim(1, 1);
await gate.WaitAsync();
gate.Release();
bool rejected = false;
try { gate.Release(); } catch (SemaphoreFullException) { rejected = true; }
Console.WriteLine($"rejected:{rejected}");
Console.WriteLine($"permits:{gate.CurrentCount}");
Microsoft documentation ↗
CLR & Concurrency

Draining data and observing completion are separate duties

Accepted values survive writer completion and terminal errors remain observable.

Read explanation & example

A channel writer can stop admission while accepted values remain buffered. The reader owns draining those values and observing the terminal task, including any failure supplied by the completion owner. The sample completes successfully after one accepted value, reads it, and then awaits the actual Reader.Completion task. A later write fails because admission has ended. This is real System.Threading.Channels behavior in a bounded single-flow program, not a durable queue or a distributed message broker. A faulted completion would require a different expected outcome even after all values were drained. Canceling one reader's wait does not authorize completing the shared writer, and an empty buffer does not by itself mean the producer has finished. Define which component owns completion and how all consumers learn about terminal failure before building a larger worker pipeline.

using System;
using System.Threading.Channels;
var channel = Channel.CreateBounded<int>(1);
channel.Writer.TryWrite(9);
channel.Writer.TryComplete();
Console.WriteLine($"item:{await channel.Reader.ReadAsync()}");
await channel.Reader.Completion;
Console.WriteLine($"completed:{channel.Reader.Completion.IsCompletedSuccessfully}");
Console.WriteLine($"write:{channel.Writer.TryWrite(10)}");
Microsoft documentation ↗
Production engineering

SQL NULL is not a C# default value

Unknown predicates, row counts and non-NULL counts are different contracts.

Read explanation & example

The companion explicitly returns an unknown comparison and counts known values in a fixed nullable collection. It is not a SQL engine. Real SQLite evidence comes from the external fixture: equality with NULL does not select those rows, IS NULL does, COUNT(*) includes them and COUNT(amount) does not. CHECK(amount>0) permits NULL while NOT NULL rejects it. Keep those properties independent when translating an application rule into a storage constraint.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

bool? Equal(int? left,int? right) => left.HasValue&&right.HasValue ? left.Value==right.Value : null;
int?[] values={null,null,1,2,3}; Console.WriteLine(Equal(null,null)?.ToString()??"unknown"); Console.WriteLine("rows:"+values.Length); Console.WriteLine("known:"+values.Count(x=>x.HasValue));
Source documentation ↗
Production engineering

An inbox key must bind the meaningful effect

Ignoring an amount while comparing identity can silently accept incompatible replay.

Read explanation & example

The one-owner dictionary stores payload and amount together and rejects a changed tuple for an existing key. It cannot establish process coordination or durability. The external inbox stores a canonical representation and changes the local SQL effect in the same transaction, including a rollback test. This protects that database effect, not an unrelated remote action. Retention, identity ownership and any external side-effect protocol require additional design.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var seen=new Dictionary<string,(string Payload,int Amount)>(); int effects=0;
string Apply(string id,string payload,int amount) { var value=(payload,amount); if(seen.TryGetValue(id,out var prior))return prior==value?"replay":"conflict"; seen.Add(id,value); effects+=amount; return "created"; }
Console.WriteLine(Apply("demo","rune",3)); Console.WriteLine(Apply("demo","rune",3)); Console.WriteLine(Apply("demo","rune",4)); Console.WriteLine("effects:"+effects);
Microsoft documentation ↗
Production engineering

Compare the version where the write happens

A prior read is not an atomic concurrency check.

Read explanation & example

The console tests integer versions and a known weak flag only. The real endpoint parses If-Match with framework APIs, requires a precondition, uses strong comparison and places the expected version in the SQL UPDATE condition. A stale or weak condition cannot authorize the demonstrated write, and a missing precondition has its own428 response. This is concurrency control for one resource, not authentication or automatic conflict merging.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

int version=1; bool Update(int expected,bool weak) { if(weak||expected!=version)return false; version++; return true; }
Console.WriteLine("updated:"+Update(1,false)); Console.WriteLine("version:"+version); Console.WriteLine("stale:"+Update(1,false)); Console.WriteLine("weak:"+Update(2,true));
Microsoft documentation ↗
Production engineering

A database commit and cache removal are separate

Invalidate only a committed source change without claiming distributed atomicity.

Read explanation & example

The portable source and optional snapshot demonstrate one-owner invalidation ordering. The external lab rolls back a controlled SQL failure without invalidating the valid old value, then commits a change before removing the actual HybridCache key. A failure after commit but before cache removal still needs policy; ordering alone cannot make two owners one transaction. Versioned keys, expiry or reconciliation can address different needs, but each requires its own evidence.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

string source="rune"; string? cached=null; int reads=0; string Read() { if(cached is null) { reads++; cached=source; } return cached; }
Console.WriteLine(Read()); string rejectedCandidate="unused"; _=rejectedCandidate; Console.WriteLine(Read()); source="glyph"; cached=null; Console.WriteLine(Read()); Console.WriteLine("reads:"+reads);
Microsoft documentation ↗
Production engineering

Forwarded context is not encrypted transport

Only configured immediate peers may supply trusted forwarding metadata.

Read explanation & example

The companion compares fixed IP addresses against a known address. It is not a header parser or network proof. Real middleware in the external lab ignores unknown peers and applies explicitly trusted forwarded context. Observing Request.Scheme as https does not mean the plain loopback connection was encrypted, nor does a forwarded client address authenticate a person. Proxy trust, host filtering, TLS termination and authorization have separate contracts.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var known=System.Net.IPAddress.Parse("203.0.113.10"); foreach(string text in new[]{"127.0.0.1","203.0.113.10"}) { bool trusted=known.Equals(System.Net.IPAddress.Parse(text)); Console.WriteLine(trusted?"https":"http"); }
Microsoft documentation ↗
Production engineering

A long-lived worker needs short-lived operation scopes

Do not capture a request-owned dependency in an application-owned worker.

Read explanation & example

The portable using scopes demonstrate ordinary deterministic disposal. The actual hosted worker creates a real async DI scope per accepted item and disposes it before signaling success, with independent created/disposed/live observations. Scope ownership does not make dependencies thread-safe or make the bounded in-memory queue durable. Graceful stop gives owned work a budget, while a crash or cancellation has a different completion contract.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

for(int i=0;i<2;i++) { using var item=new Owned(); Console.WriteLine("scope:"+item.Id); }
Console.WriteLine("live:"+Owned.Live); Console.WriteLine("disposed:"+Owned.Disposed);
sealed class Owned:IDisposable { private static int next; public static int Live; public static int Disposed; private bool disposed; public int Id {get;}=++next; public Owned(){Live++;} public void Dispose(){if(disposed)return;disposed=true;Live--;Disposed++;} }
Microsoft documentation ↗
Interview practice

Error precedence belongs to the contract

Validate malformed input before applying availability rules.

Read explanation & example

A decision can fail more than one rule at once. The order in which the public contract classifies that input is observable behavior, even if both implementations eventually refuse it. This card uses a pure stock decision whose invalid quantity or invalid stock takes precedence over insufficient availability. It deliberately returns named outcomes rather than throwing for one case and returning false for another. The program tests one ordinary rejection and one malformed combination. Neither decision reserves real inventory, and changing the rule order during a refactor would need an intentional contract change plus a new expected result.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

Console.WriteLine(Decide(4,3)); Console.WriteLine(Decide(0,-1));
static string Decide(int quantity,int stock) => quantity<=0 || stock<0 ? "invalid" : quantity>stock ? "insufficient" : "accepted";
Microsoft documentation ↗
Interview practice

Count source visits before claiming an optimization

Big-O, number of traversals and allocated storage are different dimensions.

Read explanation & example

A one-pass aggregate can compute several scalar statistics without retaining the individual elements. The code explicitly counts iterator visits while deriving a sum and a maximum. It handles the empty-source maximum through a nullable value, rather than inventing zero as a universal identity for maximum. Both statistics share the same traversal. This establishes traversal count and result behavior, not actual allocated bytes, duration or a performance advantage on every input. A materialized snapshot may still be appropriate when later consumers need random access or repeatability. Explain those ownership requirements before removing a deliberate copy from another implementation.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

int visits=0; IEnumerable<int> Source(){foreach(int x in new[]{-3,-1,-2}){visits++;yield return x;}}
long sum=0; int? maximum=null; foreach(int x in Source()){sum=checked(sum+x);maximum=maximum is null?x:Math.Max(maximum.Value,x);}
Console.WriteLine($"sum:{sum};max:{maximum};visits:{visits}");
Microsoft documentation ↗
Interview practice

Identity and value equality answer different questions

Repeated data does not imply a repeated object.

Read explanation & example

Two node objects may carry equal values while representing different locations in a graph. A structural record can additionally compare equal by its fields, so ordinary equality is not always a reliable identity test. The program creates two equal-valued records and a second reference to the first object, then prints value equality alongside ReferenceEquals. Use reference identity when the task asks whether a traversal revisits the same node. Use value equality when the domain deliberately treats equivalent data as interchangeable. Choosing either by habit changes correctness, and the choice must also be reflected in any visited-set comparer.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

var a=new Item(7);var b=new Item(7);var alias=a;Console.WriteLine("value:"+(a==b).ToString().ToLowerInvariant());Console.WriteLine("identity:"+ReferenceEquals(a,b).ToString().ToLowerInvariant());Console.WriteLine("alias:"+ReferenceEquals(a,alias).ToString().ToLowerInvariant());
sealed record Item(int Value);
Microsoft documentation ↗
Interview practice

Ordering uses signs, not arithmetic distance

Use comparison primitives and an explicit tie breaker.

Read explanation & example

A comparator result promises a sign, not a numeric distance that callers should add or negate freely. This card sorts integer pairs by score descending and ID ascending, using CompareTo for each component. The secondary ID makes equal scores deterministic when IDs are unique. Subtracting scores could overflow at extremes, and assuming stable input order would encode a different tie policy. Law tests should cover equal values, opposite extremes and three-way transitivity. The small program establishes one concrete ordering; it does not prove every possible custom comparator correct merely because a sorting call finished successfully.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

var rows=new[]{(Score:int.MinValue,Id:2),(Score:int.MaxValue,Id:3),(Score:int.MaxValue,Id:1)};Array.Sort(rows,Comparer<(int Score,int Id)>.Create((a,b)=>{int c=b.Score.CompareTo(a.Score);return c!=0?c:a.Id.CompareTo(b.Id);}));Console.WriteLine(string.Join(",",rows.Select(x=>x.Id)));
Microsoft documentation ↗
Interview practice

Segmentation does not establish text equality

Equal text-element counts can still represent different ordinal strings.

Read explanation & example

A precomposed accented letter and its decomposed base-plus-mark spelling can each form one text element while remaining different ordinal strings. The program demonstrates both facts and prints their different UTF-16 lengths. It preserves the original strings and deliberately does not normalize them. Segmentation determines boundaries, while normalization changes representation according to a separately chosen policy. Full Unicode normalization also depends on runtime globalization support. In .NET9 invariant globalization mode, Normalize returns the input rather than performing the usual linguistic normalization, so this portable card does not promise a normalization result. A product needs verified runtime support, an explicit normalization form, a comparer and a policy for invalid input or confusable characters. Removing combining marks by hand is not an equivalent general normalization algorithm, and equal text-element counts alone cannot establish identifier equality or security.

using System;
using System.Globalization;

string a="\u00E9",b="e\u0301";
Console.WriteLine("ordinal:"+string.Equals(a,b,StringComparison.Ordinal).ToString().ToLowerInvariant());
Console.WriteLine("elements:"+StringInfo.ParseCombiningCharacters(a).Length+","+StringInfo.ParseCombiningCharacters(b).Length);
Console.WriteLine("UTF16:"+a.Length+","+b.Length);
Microsoft documentation ↗
Interview practice

An oracle must encode the public meaning

A count check alone cannot verify first-occurrence selection.

Read explanation & example

A test oracle needs to encode the property the caller actually requested. If a method promises the first unique value, checking only that the result occurs once leaves a wrong minimum-unique implementation undetected. The program uses an input with two unique values in a deliberately different numeric order. It checks both uniqueness and first position. A generated test portfolio should preserve such discriminating examples even after randomized exploration finds more cases. An independent slower oracle is useful for small inputs, but agreement between two copies of the same mistaken rule remains weak evidence. State the rule before writing either implementation.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

int[] values={9,2,2,1};int result=values.First(x=>values.Count(y=>y==x)==1);Console.WriteLine("first:"+result);Console.WriteLine("minimum unique:"+values.Where(x=>values.Count(y=>y==x)==1).Min());Console.WriteLine("contract differs:"+(result!=1).ToString().ToLowerInvariant());
Microsoft documentation ↗
Advanced Memory

Minor collections and old-to-young references

Trace a write barrier, distinguish a collection set from the full graph, and test an old owner with a young child.

Read explanation & example

Suppose a long-lived cache object stores a newly allocated payload. Following only stack roots and references inside the young collection area would miss that payload. CoreCLR maintains bookkeeping around relevant managed reference writes so a young collection can discover references coming from older storage. The write barrier is runtime machinery attached to reference updates; ordinary application code should use managed assignments rather than trying to maintain collector metadata itself. A remembered reference is evidence that a young object might still be needed, not evidence that the containing cache is useful to the application forever. The cost of a collection is also not determined only by the number of bytes allocated since the previous one. Surviving objects, scanned references, promotion and remembered locations all contribute. A program that creates many short-lived independent byte arrays can behave differently from one that continually inserts small reference-rich nodes into an old collection. Compare the same request shape and retention policy before interpreting a change in collection frequency as a regression. A lower collection count can coexist with a larger retained heap.

var cache = new Cache();
cache.Entry = new Payload("first");
var previous = cache.Entry;
cache.Entry = new Payload("second");
Console.WriteLine($"current:{cache.Entry.Name}");
Console.WriteLine($"previous:{previous.Name}");
cache.Entry = null;
Console.WriteLine($"cache-empty:{cache.Entry is null}");
GC.KeepAlive(previous);
sealed class Cache { public Payload? Entry; }
sealed record Payload(string Name);
Microsoft documentation ↗
Advanced Memory

Async, callbacks and cancellation ownership

Follow captured state through pending work and end cancellation registrations at their real lifetime boundary.

Read explanation & example

The portable cancellation callback is static, so it cannot implicitly capture surrounding locals. Nevertheless the registration receives state explicitly and retains that state for the registration's lifetime. Static is a lexical capture restriction, not a promise of zero allocation, zero ownership or collection independence. A static delegate plus a large state object can retain just as much payload as a capturing delegate. A CancellationTokenRegistration represents a lifecycle obligation. Disposing the registration unregisters the callback and may coordinate with callback execution. The exact concurrency contract matters when cancellation can run on another thread. Never hold an unrelated lock around disposal if the callback needs that same lock to finish. Requesting cancellation, waiting for the operation to complete and releasing the registration are separate events, and a robust design makes their order explicit.

using System.Threading;
var state = new RequestState();
using var source = new CancellationTokenSource();
using (source.Token.Register(static value => ((RequestState)value!).Calls++, state))
{
    source.Cancel();
    Console.WriteLine($"inside:{state.Calls}");
}
source.Cancel();
Console.WriteLine($"after:{state.Calls}");
Console.WriteLine($"cancelled:{source.IsCancellationRequested}");
sealed class RequestState { public int Calls; }
Microsoft documentation ↗
Advanced Memory

Pinned lifetimes and the pinned-object heap

Separate strong ownership, pin duration, POH allocation and native pointer validity.

Read explanation & example

Pinning an already allocated array does not migrate it to the pinned-object heap. Moving an object simply because the caller wants its current address fixed would itself require coordination and movement, undermining the operation's purpose. CoreCLR instead exposes allocation-time pinning APIs for suitable scenarios. An array allocated pinned can be placed in POH from its creation rather than moving an existing object there later. POH still participates in managed lifetime management. An allocation-pinned array is not immortal, and retaining a pin is not equivalent to leaking a native allocation forever. A normal managed owner can keep it live; when the required strong ownership ends, a collection including the appropriate heap can reclaim it. POH avoids mixing those deliberately pinned allocations with ordinary movable storage, but it does not remove allocation cost, fragmentation concerns or the need for a bounded pool.

using System.Runtime.InteropServices;
var buffer = new byte[] { 4, 8, 15, 16 };
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
    Console.WriteLine($"handle:{handle.IsAllocated}");
    buffer[0] = 42;
    Console.WriteLine($"first:{buffer[0]}");
}
finally { handle.Free(); }
Console.WriteLine($"released:{!handle.IsAllocated}");
GC.KeepAlive(buffer);
Source documentation ↗
Advanced Memory

Span, Memory and hidden backing-store retention

Expose the difference between a view, a copy, an owner and a valid buffer lease.

Read explanation & example

Memory<T> can be stored in ordinary managed objects and used across asynchronous operations, but it is not itself an ownership-transfer or disposal token. IMemoryOwner<T> expresses a distinct ownership abstraction. A consumer needs a valid lease on the memory for the entire operation, and the owner must not dispose or return backing storage while that lease remains active. Merely keeping a Memory value reachable does not extend a pool's valid lease after Return. Span<T> is a ref-like view with language restrictions designed around safe lifetimes. Modern C# permits more useful ref-like code in async methods under specific constraints, but a span still cannot simply remain live across an arbitrary suspension point. Do not reduce the rule to an outdated slogan that no async method may ever mention Span, or the equally wrong claim that a heap-backed array makes every span lifetime unrestricted. Follow the compiler's actual restrictions and the API's ownership contract.

using System.Runtime.InteropServices;
var source = new byte[1_048_576]; source[0] = 7;
ReadOnlyMemory<byte> view = source.AsMemory(0, 12);
MemoryMarshal.TryGetArray(view, out ArraySegment<byte> segment);
var copy = view.ToArray();
Console.WriteLine($"view:{view.Length};backing:{segment.Array!.Length};copy:{copy.Length}");
source[0] = 9;
Console.WriteLine($"view-first:{view.Span[0]};copy-first:{copy[0]}");
Microsoft documentation ↗
Advanced Memory

Measure allocations without confusing live memory

Warm the code, keep the measured work synchronous, and separate allocation throughput from retention.

Read explanation & example

GC.GetAllocatedBytesForCurrentThread reports allocations attributed to the current thread. It does not measure native allocations, the live heap, or every allocation caused by a request that resumes on a different thread after await. A before/after subtraction across arbitrary asynchronous work can therefore mix unrelated activity and miss some of the operation's allocations. Keep a thread-local experiment synchronous or use an appropriate process-wide measurement and controlled workload. Warmup, JIT compilation, lazy initialization, logging and result formatting can contaminate the measured interval. Print after taking the final counter. Preserve enough work that optimization does not eliminate the behavior under investigation, but recognize that adding an escape or retaining all results changes the workload. A benchmark that stores every result measures a different lifetime pattern from a production path that discards each result immediately.

int value = 7;
object box = value;
value = 9;
Console.WriteLine($"value:{value};boxed:{box}");
Console.WriteLine($"same-box:{ReferenceEquals(box, box)}");
Console.WriteLine($"new-box:{ReferenceEquals(box, (object)7)}");
Console.WriteLine("allocated-bytes:not-inferred-from-identity");
Microsoft documentation ↗
Advanced Memory

GC modes, DATAS and container budgets

Read effective runtime settings and distinguish a managed heap limit from a whole-process memory limit.

Read explanation & example

Dynamic adaptation to application sizes is a CoreCLR policy intended to make heap sizing respond to application characteristics, including long-lived data. Current .NET documentation states that DATAS is enabled by default starting with .NET 9. Treat that as a versioned policy fact, not a universal property of every CLR, historical release or managed runtime. An explicit deployment override and effective runtime version still matter. Adaptive sizing does not eliminate a memory budget or make poor ownership harmless. A cache that retains every response remains a growing live set. More aggressive memory conservation can trade throughput or collection work against retained capacity. Compare modes with the same input distribution, concurrency, warmup and observation window. A smaller heap in an idle demonstration is not sufficient evidence that a production service meets its latency objective under bursts.

using System.Runtime;
using System.Runtime.InteropServices;
Console.WriteLine($"framework:{RuntimeInformation.FrameworkDescription}");
Console.WriteLine($"server-gc:{GCSettings.IsServerGC}");
Console.WriteLine($"max-generation:{GC.MaxGeneration}");
Console.WriteLine($"processors:{Environment.ProcessorCount}");
Console.WriteLine("runtime-observation:values-vary");
Microsoft documentation ↗