# C# Programming

> Comprehensive C# 14 and .NET 10 notes covering the CLR, type system, OOP, generics, LINQ, async/await, threads, atomicity, channels, async streams, memory management, pipelines, I/O, networking, reflection, and production performance.

- Author: Muhammet Ali Köker
- Language: en
- Canonical: https://alikoker.com.tr/en/csharp-programming
- Translation: https://alikoker.com.tr/csharp-programlama
- Published: 2015-07-01T18:34:11+03:00
- Modified: 2026-09-16T02:53:58+03:00
- Verified: 2026-09-16T02:53:58+03:00
- Type: article

C# combines static type safety, an object-oriented model, functional expression tools, asynchronous programming, and runtime services in one language. Three layers should be kept distinct: C# is the source language, the compiler turns source into intermediate code and metadata, and the .NET runtime loads and executes assemblies.

These notes focus on semantics and production behavior rather than memorizing syntax. Fundamental constructs are connected to generics, LINQ, concurrency, memory, networking, and reflection so that language features can be evaluated with their runtime costs.

## 1. C#, .NET, and the CLR

Modern .NET is not the Windows-only .NET Framework model of earlier releases. .NET 10 is a long-term support release and supports C# 14. The CLR provides assembly loading, JIT compilation, garbage collection, exception propagation, threading services, and runtime type safety.

C# source code is not processor instruction code. Compilation produces Common Intermediate Language and metadata inside an assembly. The runtime can translate required methods to native machine code. Native AOT provides an ahead-of-time native compilation option for suitable applications.

```csharp
using System;

public static class Program
{
    public static void Main()
    {
        Console.WriteLine($".NET: {Environment.Version}");
        Console.WriteLine($"64-bit process: {Environment.Is64BitProcess}");
    }
}
```

Managed code benefits from memory safety and runtime services. External resources such as file handles, sockets, database connections, and native handles still require explicit lifetime management.

## 2. Build, SDK, and project model

The current command-line workflow is centered on the `dotnet` CLI:

```text
dotnet new console -n Sample
dotnet build Sample
dotnet run --project Sample
```

The SDK project file controls the target framework, nullable analysis, implicit usings, and other compilation settings. A production language feature must be supported by the SDK and compiler used throughout the delivery pipeline, not only by a developer's IDE.

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
```

## 3. Type system: value and reference semantics

The type system controls more than the set of values a variable may hold. It also influences copying, nullability, boxing, and memory behavior. `int`, `double`, `bool`, `char`, `decimal`, and user-defined structs are value types. Classes, arrays, delegates, and `string` are reference types.

`decimal` is designed for decimal arithmetic where binary floating-point representation error is undesirable. `double` is typically preferred for scientific and general numerical work.

```csharp
decimal net = 1250.75m;
decimal taxRate = 0.20m;
decimal total = net * (1m + taxRate);

int count = 42;
long widened = count;

checked
{
    int narrowed = checked((int)3_000_000_000L);
    Console.WriteLine(narrowed);
}
```

The final conversion throws `OverflowException`. `checked` is valuable when wraparound would violate a correctness invariant.

## 4. Nullable types and null safety

Nullable reference types are a compiler analysis feature rather than a different runtime representation. `string` expresses a non-null expectation while `string?` marks a reference that may be null.

```csharp
static int LengthOrZero(string? value)
{
    return value?.Length ?? 0;
}

string? input = Console.ReadLine();
Console.WriteLine(LengthOrZero(input));
```

The null-forgiving operator `!` performs no runtime check. It should only be used when an external invariant really guarantees non-nullness.

## 5. Operators, short-circuiting, and bit manipulation

`&&` and `||` short-circuit their right operand. `&`, `|`, `^`, `~`, `<<`, and `>>` are useful for masks, protocol fields, and low-level data manipulation.

```csharp
const byte Valid = 0b0000_0001;
const byte Encrypted = 0b0000_0010;

byte flags = Valid | Encrypted;

bool valid = (flags & Valid) != 0;
bool encrypted = (flags & Encrypted) != 0;

Console.WriteLine($"{valid} {encrypted}");
```

Named constants or a `[Flags]` enum are usually safer than unexplained numeric masks.

## 6. Control flow and pattern matching

Classic `if` and `switch` remain fundamental. Modern C# extends them with type, constant, relational, and property patterns.

```csharp
static string Classify(int latencyMs) => latencyMs switch
{
    < 0 => throw new ArgumentOutOfRangeException(nameof(latencyMs)),
    < 20 => "very low",
    < 100 => "low",
    < 500 => "moderate",
    _ => "high"
};

Console.WriteLine(Classify(87));
```

Pattern matching can remove incidental branching syntax, but large business rule sets may still be better represented by explicit policy objects or decision tables.

## 7. Loops and the enumerator model

Use `for` when an index or explicit counter is part of the operation, `foreach` for element traversal, `while` when the condition is tested before the body, and `do-while` when at least one execution is required.

```csharp
int[] samples = [12, 18, 21, 17, 25];

long sum = 0;
foreach (int sample in samples)
{
    sum += sample;
}

double average = (double)sum / samples.Length;
Console.WriteLine(average);
```

`foreach` also works with types that follow the enumerator pattern; it is not limited to `IEnumerable<T>`.

## 8. Methods and parameter passing

Arguments are passed by value by default. For a reference type, the copied value is the reference, not the object itself. `ref`, `out`, and `in` change parameter-passing semantics.

```csharp
static bool TryNormalize(int value, out double normalized)
{
    if (value is < 0 or > 100)
    {
        normalized = 0;
        return false;
    }

    normalized = value / 100.0;
    return true;
}

if (TryNormalize(73, out double result))
{
    Console.WriteLine(result);
}
```

For multiple return values, a tuple or a small record/struct can often be clearer than several `out` parameters. `ref` is most useful in low-level APIs and where copying a large value type is demonstrably significant.

## 9. Classes, encapsulation, and object lifetime

A constructor should establish a valid initial state. Public fields generally expose too much mutable state; properties and methods can preserve invariants.

```csharp
public sealed class RateLimiter
{
    private int _limit;

    public RateLimiter(int limit)
    {
        Limit = limit;
    }

    public int Limit
    {
        get => _limit;
        set => _limit = value > 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value));
    }
}
```

C# 14 can use the `field` keyword to validate a compiler-generated backing field:

```csharp
public sealed class Endpoint
{
    public string Name
    {
        get;
        set => field = string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Name cannot be empty.", nameof(value))
            : value.Trim();
    } = "default";
}
```

The second example requires C# 14.

## 10. Records, equality, and immutability

Reference identity and value equality are different concepts. Records provide value-oriented equality for data-centric models.

```csharp
public sealed record SensorReading(string Source, long Sequence, double Value);

SensorReading a = new("A1", 7, 42.5);
SensorReading b = new("A1", 7, 42.5);

Console.WriteLine(a == b);

SensorReading c = a with { Sequence = 8 };
Console.WriteLine(c);
```

If a custom class implements equality, `Equals` and `GetHashCode` must satisfy the same equality contract. Mutable hash keys are especially dangerous in hash-based collections.

## 11. Inheritance, composition, and polymorphism

Inheritance should model a genuine substitutable relationship rather than being a shortcut for code reuse. Composition and interfaces are often more flexible for behavior variation.

```csharp
public interface IFormatter
{
    string Format(double value);
}

public sealed class FixedFormatter : IFormatter
{
    public string Format(double value) => value.ToString("F2");
}

public sealed class SampleWriter
{
    private readonly IFormatter _formatter;

    public SampleWriter(IFormatter formatter)
    {
        _formatter = formatter;
    }

    public string Write(double value) => _formatter.Format(value);
}
```

`virtual` and `override` provide dynamic dispatch. `sealed` can close either a type or an override chain when further extension is undesirable.

## 12. Interfaces, structs, enums, and boxing

Structs work best as small value-oriented types and should usually be immutable. `readonly record struct` is a useful option for compact value models.

```csharp
public enum PacketState : byte
{
    Unknown,
    Ready,
    Sent,
    Failed
}

public readonly record struct Coordinate(double X, double Y);

Coordinate p = new(12.5, 8.25);
PacketState state = PacketState.Ready;
Console.WriteLine($"{p} {state}");
```

Converting a value type to `object` or to an implemented interface can box it. Repeated boxing in a hot path creates allocations and GC pressure.

## 13. Arrays and `Span<T>`

Arrays are fixed-length reference types. Rectangular multidimensional arrays use `T[,]`; jagged arrays use `T[][]`. `Span<T>` and `ReadOnlySpan<T>` can create views over existing memory without an additional allocation.

```csharp
static int SumPositive(ReadOnlySpan<int> values)
{
    int sum = 0;

    foreach (int value in values)
    {
        if (value > 0)
        {
            sum += value;
        }
    }

    return sum;
}

int[] values = [4, -2, 7, 3];
Console.WriteLine(SumPositive(values.AsSpan(1, 3)));
```

A span is a `ref struct`, so its lifetime is deliberately constrained to keep low-allocation memory access safe.

## 14. Strings, formatting, and culture

`string` is immutable. Repeated concatenation in a loop may justify `StringBuilder`. Protocol serialization and user-facing formatting have different culture requirements.

```csharp
using System.Globalization;

double value = 12345.6789;

Console.WriteLine(value.ToString("F2", CultureInfo.InvariantCulture));
Console.WriteLine($"{value:N2}");
```

Machine-readable protocols should use a culture-invariant format. User interfaces should normally respect the user's culture.

## 15. Generics and collections

Prefer generic collections such as `List<T>`, `Dictionary<TKey,TValue>`, `HashSet<T>`, `Queue<T>`, and `Stack<T>` over legacy `ArrayList` and `Hashtable`.

```csharp
var latest = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
    ["A1"] = 12.4,
    ["B7"] = 19.8
};

latest["A1"] = 13.1;

if (latest.TryGetValue("a1", out double current))
{
    Console.WriteLine(current);
}
```

Select a collection by access pattern: sequential/indexed access, keyed lookup, membership test, queueing, or stack behavior.

## 16. LINQ and deferred execution

LINQ supplies a common query model over sequences. Many `IEnumerable<T>` operators are deferred; execution begins when the result is enumerated.

```csharp
int[] samples = [4, 11, 7, 21, 9, 16];

var query = samples
    .Where(static x => x >= 10)
    .OrderBy(static x => x)
    .Select(static x => x * 2);

foreach (int item in query)
{
    Console.WriteLine(item);
}
```

Terminal operations such as `ToArray`, `ToList`, and many aggregations force evaluation. `IQueryable<T>` can translate an expression tree to another query language, so its cost model is not the same as in-memory LINQ.

## 17. Delegates, lambdas, and events

A delegate is a typed callable reference. Lambdas can target delegates or expression trees. An event preserves control over subscription while preventing external code from directly invoking the publisher's delegate.

```csharp
public sealed class ThresholdMonitor
{
    public event EventHandler<double>? Exceeded;

    public void Observe(double value)
    {
        if (value > 80)
        {
            Exceeded?.Invoke(this, value);
        }
    }
}

var monitor = new ThresholdMonitor();
monitor.Exceeded += static (_, value) => Console.WriteLine($"Threshold exceeded: {value}");
monitor.Observe(91.5);
```

A long-lived publisher can retain a short-lived subscriber through an event subscription, so unsubscription and ownership still matter.

## 18. Exception handling

Exceptions are appropriate for exceptional failures or contract violations that should not be encoded as a normal result. They are expensive when used as routine control flow.

```csharp
static int ParsePort(string text)
{
    if (!int.TryParse(text, out int port) || port is < 1 or > 65535)
    {
        throw new ArgumentOutOfRangeException(nameof(text), "A valid TCP/UDP port is required.");
    }

    return port;
}

try
{
    Console.WriteLine(ParsePort("70000"));
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine(ex.Message);
}
```

Use `throw;` to rethrow while preserving the original stack trace. `using` and `await using` usually express resource cleanup more clearly than a manual `finally`.

## 19. Resource ownership: `IDisposable` and `IAsyncDisposable`

Garbage collection reclaims managed memory; it does not guarantee timely release of operating-system resources.

```csharp
using FileStream stream = File.OpenRead("data.bin");

byte[] buffer = new byte[4096];
int read = stream.Read(buffer, 0, buffer.Length);

Console.WriteLine(read);
```

Finalizers are a fallback rather than the normal ownership mechanism. Native-handle ownership is often safer through `SafeHandle`.

## 20. File I/O and modern asynchronous APIs

`FileStream`, `StreamReader`, `StreamWriter`, `BinaryReader`, and `BinaryWriter` remain valid building blocks. The `File` convenience API is appropriate for small operations.

```csharp
static async Task<long> CountLinesAsync(string path, CancellationToken cancellationToken)
{
    long count = 0;

    await using FileStream stream = new(
        path,
        FileMode.Open,
        FileAccess.Read,
        FileShare.Read,
        bufferSize: 64 * 1024,
        options: FileOptions.Asynchronous | FileOptions.SequentialScan);

    using var reader = new StreamReader(stream);

    while (await reader.ReadLineAsync(cancellationToken) is not null)
    {
        count++;
    }

    return count;
}
```

Streaming large files provides more predictable memory usage than loading all content at once.

## 21. `async`/`await`, `Task`, and ownership of work

Marking a method `async` does not move it to a background thread. It runs synchronously until it reaches an incomplete awaitable; `await` then registers the continuation without blocking the current thread. This is the basis of scalable asynchronous I/O.

CPU-bound work and I/O waits must be treated differently. If an API already exposes true asynchronous I/O, wrapping it in `Task.Run` consumes a thread without making the I/O more asynchronous. `Task.Run` is useful for offloading CPU-bound work, especially when a UI thread must remain responsive.

```csharp
static async Task<string> DownloadWithDeadlineAsync(
    HttpClient client,
    Uri address,
    CancellationToken cancellationToken)
{
    Task<string> download = client.GetStringAsync(address, cancellationToken);

    return await download.WaitAsync(
        TimeSpan.FromSeconds(3),
        cancellationToken);
}
```

A `Task` also represents ownership of an in-flight operation. A task should be awaited, returned, tracked, or otherwise deliberately observed. Losing a task as "fire and forget" makes exception handling, shutdown, and resource lifetime ambiguous.

`async void` should normally be limited to contracts that require `void`, such as event handlers.

## 22. Asynchrony, concurrency, and parallelism

These terms describe different properties:

- **Asynchrony** avoids tying up a thread while waiting.
- **Concurrency** means multiple operations have overlapping lifetimes.
- **Parallelism** means operations make progress at the same time on distinct execution resources.

Awaiting HTTP I/O normally does not create parallel CPU execution. `Parallel.For`, on the other hand, can distribute CPU-bound computation across cores. A server can also handle a large number of concurrent requests through asynchronous I/O without assigning one dedicated thread per request.

Architecture should begin with the workload: CPU or I/O, shared state or isolated state, ordering requirements, and the intended concurrency limit.

## 23. Threads and direct `Thread` usage

A `Thread` represents an operating-system scheduled execution path. Threads in the same process can share heap objects while each thread has its own call stack and execution state. The shared address space makes communication cheap, but also enables races and visibility problems.

Most short- and medium-lived modern .NET work should use tasks, the thread pool, and asynchronous APIs rather than creating raw threads. A dedicated thread can still be justified for a long-lived loop with specific affinity, latency, or scheduling requirements.

```csharp
var worker = new Thread(() =>
{
    while (!stopping.IsCancellationRequested)
    {
        ProcessOneItem();
    }
})
{
    IsBackground = true,
    Name = "dedicated-worker"
};

worker.Start();
```

Thread priority is not a hard real-time guarantee. The GC, OS scheduler, other processes, and hardware can still introduce latency.

## 24. The thread pool and task scheduling

Creating an OS thread has stack, kernel-object, and scheduling costs. The .NET thread pool reuses worker threads and adjusts their number as load changes. `Task.Run` is the normal high-level entry point for short CPU-bound work on the default pool.

The major operational risk is **thread-pool starvation caused by blocking**. If many pool threads are held in `.Result`, `.Wait()`, long critical sections, or synchronous I/O, newly queued work may wait for available workers. Naturally asynchronous paths should therefore remain asynchronous end to end.

```csharp
static async Task<int[]> LoadAllAsync(
    HttpClient client,
    IReadOnlyList<Uri> addresses,
    CancellationToken cancellationToken)
{
    Task<int>[] jobs = addresses
        .Select(async address =>
        {
            byte[] body = await client.GetByteArrayAsync(address, cancellationToken);
            return body.Length;
        })
        .ToArray();

    return await Task.WhenAll(jobs);
}
```

For very large input sets, unbounded concurrency is still dangerous; use a semaphore or a bounded channel.

## 25. Shared state, races, and atomicity

A single source-code statement is not necessarily atomic. `counter++` can decompose into read, modify, and write steps; two executors can interleave those steps and lose an update.

```csharp
public sealed class UnsafeCounter
{
    private int _value;

    public void Increment() => _value++;
    public int Value => _value;
}
```

The first design choice should be to reduce shared mutable state. Immutable objects, message passing, single-owner state, and result aggregation often remove synchronization requirements entirely.

## 26. `Interlocked`, `Volatile`, and memory visibility

`Interlocked` supplies atomic read-modify-write operations for a single memory location.

```csharp
public sealed class AtomicCounter
{
    private long _value;

    public long Increment() => Interlocked.Increment(ref _value);
    public long Value => Interlocked.Read(ref _value);
}
```

Compare-and-swap can implement more advanced lock-free updates:

```csharp
static double Max(ref double location, double candidate)
{
    while (true)
    {
        double snapshot = Volatile.Read(ref location);
        if (snapshot >= candidate)
            return snapshot;

        double observed = Interlocked.CompareExchange(
            ref location,
            candidate,
            snapshot);

        if (observed == snapshot)
            return candidate;
    }
}
```

`volatile` affects visibility and ordering for specific accesses, but it does not make compound operations such as `x++` atomic. If correctness depends on an invariant across several fields, a lock or a higher-level concurrent structure is usually easier to prove correct.

## 27. `lock`, `Monitor`, and `System.Threading.Lock`

Mutual exclusion allows one executor at a time to modify a critical region. Starting with .NET 9 and C# 13, a dedicated `System.Threading.Lock` instance is available for general locking and works directly with the C# `lock` statement.

```csharp
public sealed class Balance
{
    private readonly System.Threading.Lock _gate = new();
    private decimal _value;

    public void Add(decimal amount)
    {
        lock (_gate)
        {
            _value += amount;
        }
    }

    public decimal Read()
    {
        lock (_gate)
        {
            return _value;
        }
    }
}
```

Do not expose the lock object. Avoid locking on `this`, `Type` instances, or interned strings because unrelated code may acquire the same lock.

An `await` expression cannot appear inside a `lock` body. If mutual exclusion must span asynchronous waits, use an async-compatible primitive such as `SemaphoreSlim.WaitAsync`.

`Monitor` exposes the lower-level mechanics behind classic monitor locking, including `Wait`, `Pulse`, and `PulseAll`. These can express custom condition protocols, but channels, semaphores, and other high-level primitives are generally easier to compose safely.

## 28. Deadlock, starvation, livelock, and contention

A race-free program can still fail to make progress.

A classic deadlock pattern is inconsistent lock ordering:

```text
Work A: lock X -> lock Y
Work B: lock Y -> lock X
```

A stable lock hierarchy is one of the strongest preventive measures. Also avoid holding a lock across I/O, external callbacks, or work whose duration is not under your control.

**Starvation** occurs when one executor repeatedly loses access to a resource. **Livelock** occurs when executors remain active but keep reacting to each other without completing useful work.

**Contention** is not a correctness failure, but it can dominate tail latency. Shorter critical sections, partitioned state, immutable snapshots, and message passing are common ways to reduce it.

## 29. Limiting concurrency with `SemaphoreSlim`

A resource may support several concurrent operations without supporting an unlimited number. `SemaphoreSlim` is a practical asynchronous limiter for remote calls, database work, or disk activity.

```csharp
public sealed class LimitedFetcher
{
    private readonly SemaphoreSlim _slots = new(initialCount: 8);

    public async Task<byte[]> FetchAsync(
        HttpClient client,
        Uri address,
        CancellationToken cancellationToken)
    {
        await _slots.WaitAsync(cancellationToken);
        try
        {
            return await client.GetByteArrayAsync(address, cancellationToken);
        }
        finally
        {
            _slots.Release();
        }
    }
}
```

For fairness, priority, or queue-level policies, use a dedicated work queue or channel rather than treating a semaphore as a complete scheduler.

## 30. Signaling primitives

Locking controls who may access a region; signaling coordinates when another activity may proceed.

- `AutoResetEvent` releases one waiter for each signal.
- `ManualResetEvent` and `ManualResetEventSlim` act as gates that remain open until reset.
- `CountdownEvent` completes after a configured number of signals.
- `Barrier` coordinates participants across phased parallel work.
- `Mutex` can provide cross-process mutual exclusion when that is genuinely required.

Many wait-handle primitives block threads. In asynchronous application code, tasks, channels, and `SemaphoreSlim.WaitAsync` are often more scalable choices.

## 31. `ReaderWriterLockSlim` for read-heavy state

When state is read frequently and changed rarely, permitting multiple simultaneous readers can reduce contention. `ReaderWriterLockSlim` supports read, write, and upgradeable-read modes.

```csharp
public sealed class NameCache
{
    private readonly ReaderWriterLockSlim _gate = new();
    private readonly Dictionary<int, string> _items = [];

    public bool TryGet(int id, out string? value)
    {
        _gate.EnterReadLock();
        try
        {
            return _items.TryGetValue(id, out value);
        }
        finally
        {
            _gate.ExitReadLock();
        }
    }

    public void Put(int id, string value)
    {
        _gate.EnterWriteLock();
        try
        {
            _items[id] = value;
        }
        finally
        {
            _gate.ExitWriteLock();
        }
    }
}
```

A reader-writer lock is not automatically faster than a simple lock. Short critical sections, frequent writes, or low contention can erase its advantage. Measure before adopting it.

## 32. Producer/consumer pipelines, `Channel<T>`, and backpressure

Producer/consumer pipelines decouple work creation from work execution. `System.Threading.Channels` provides asynchronous channels that support multiple readers and writers.

A bounded channel introduces **backpressure** when producers outrun consumers:

```csharp
using System.Threading.Channels;

Channel<Job> queue = Channel.CreateBounded<Job>(
    new BoundedChannelOptions(256)
    {
        FullMode = BoundedChannelFullMode.Wait,
        SingleReader = false,
        SingleWriter = false
    });

static async Task ProduceAsync(
    ChannelWriter<Job> writer,
    IEnumerable<Job> jobs,
    CancellationToken cancellationToken)
{
    try
    {
        foreach (Job job in jobs)
            await writer.WriteAsync(job, cancellationToken);
    }
    finally
    {
        writer.TryComplete();
    }
}

static async Task ConsumeAsync(
    ChannelReader<Job> reader,
    CancellationToken cancellationToken)
{
    await foreach (Job job in reader.ReadAllAsync(cancellationToken))
        await ProcessAsync(job, cancellationToken);
}
```

`BlockingCollection<T>` is still useful for blocking producer/consumer code. Channels are usually a more natural fit when the entire pipeline is asynchronous.

## 33. Concurrent collections and immutable snapshots

`ConcurrentDictionary<TKey,TValue>`, `ConcurrentQueue<T>`, `ConcurrentStack<T>`, and related types make their own operations safe for concurrent callers. They do not automatically make a multi-step domain rule atomic.

A sequence such as "read, test, call another service, update" can race even if every collection method is individually thread-safe.

For read-mostly state, an immutable snapshot is another option:

```csharp
private ImmutableDictionary<string, Endpoint> _snapshot =
    ImmutableDictionary<string, Endpoint>.Empty;

public Endpoint? Find(string key)
{
    ImmutableDictionary<string, Endpoint> snapshot =
        Volatile.Read(ref _snapshot);

    return snapshot.GetValueOrDefault(key);
}
```

Writers can construct a new snapshot and publish it with a single reference change, leaving readers lock-free.

## 34. Cancellation, timeout, deadline, and graceful shutdown

`CancellationToken` is cooperative signaling, not forced thread termination. Code observes the request at safe points and stops while preserving invariants.

Cancellation and timeout should also be modeled separately. Cancellation often means the caller no longer wants the result; a timeout means a time budget was exceeded.

```csharp
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
    applicationStopping,
    requestAborted,
    deadline.Token);

await ProcessAsync(linked.Token);
```

A controlled shutdown normally stops accepting new work, signals cancellation, awaits owned tasks with a bounded deadline, completes queues, disposes resources, and records work that failed to finish.

`Thread.Abort` is not a modern replacement for this protocol.

## 35. `SynchronizationContext`, `ExecutionContext`, and continuation placement

Desktop UI frameworks often require controls to be accessed from the UI thread. `SynchronizationContext` can represent a target context to which an awaited continuation is posted.

General-purpose library code can use `ConfigureAwait(false)` where resuming on the captured context is unnecessary. Application-level code may intentionally require the UI context. ASP.NET Core should not be treated as if it had the same synchronization context as a desktop UI.

`ExecutionContext` is a different mechanism. It flows ambient logical state such as `AsyncLocal<T>` across asynchronous boundaries. `ThreadLocal<T>` follows a physical thread; `AsyncLocal<T>` follows a logical asynchronous flow.

## 36. Bridging callbacks and events with `TaskCompletionSource<T>`

Some APIs signal completion through callbacks, events, or custom protocols. `TaskCompletionSource<T>` can adapt such completion into the task-based model.

```csharp
static Task<string> WaitForMessageAsync(
    LegacyReceiver receiver,
    CancellationToken cancellationToken)
{
    var source = new TaskCompletionSource<string>(
        TaskCreationOptions.RunContinuationsAsynchronously);

    void Handler(object? sender, MessageEventArgs e)
    {
        receiver.MessageReceived -= Handler;
        source.TrySetResult(e.Message);
    }

    receiver.MessageReceived += Handler;

    cancellationToken.Register(() =>
    {
        receiver.MessageReceived -= Handler;
        source.TrySetCanceled(cancellationToken);
    });

    return source.Task;
}
```

The `TrySet...` methods are useful when several completion paths can race. `RunContinuationsAsynchronously` helps prevent arbitrary consumer continuations from running inline on the thread that completes the source.

Do not wrap an API that already returns a `Task` merely to create another `TaskCompletionSource`.

## 37. CPU-bound parallelism with `Parallel`, PLINQ, and tasks

Independent CPU-bound work can benefit from multiple cores when each unit is large enough to justify partitioning and scheduling overhead.

```csharp
double[] input = CreateInput();

Parallel.For(
    fromInclusive: 0,
    toExclusive: input.Length,
    i =>
    {
        input[i] = ExpensiveTransform(input[i]);
    });
```

PLINQ can parallelize some in-memory query pipelines:

```csharp
var result = values
    .AsParallel()
    .Where(IsCandidate)
    .Select(Score)
    .Where(static score => score > 0.80)
    .ToArray();
```

Ordering through `AsOrdered` has a cost. Side-effect-heavy queries and I/O-bound operations are poor default PLINQ candidates.

The appropriate degree of parallelism depends on more than core count: other process work, cache behavior, memory bandwidth, and downstream resource limits can dominate.

## 38. Timers and periodic work

Callback timers can overlap if a new tick arrives before the previous callback completes. `PeriodicTimer` provides a readable asynchronous loop when one logical consumer should process periodic ticks.

```csharp
static async Task RunPeriodicAsync(CancellationToken cancellationToken)
{
    using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

    while (await timer.WaitForNextTickAsync(cancellationToken))
    {
        await RefreshSnapshotAsync(cancellationToken);
    }
}
```

"Start every five seconds" and "wait five seconds after each run" are different scheduling contracts. Long-running work, drift, overlap, and shutdown behavior should be specified explicitly.

## 39. Updating historical threading APIs for modern .NET

The conceptual core of older threading material remains valuable, but several APIs should be read historically:

- `Thread.Abort` is unsupported on modern .NET and throws `PlatformNotSupportedException`; use cooperative cancellation.
- `Thread.Suspend` and `Thread.Resume` are obsolete; use synchronization and coordination primitives.
- `BackgroundWorker` belongs to an older event-based desktop asynchronous model; `Task`, `async`, and `await` are normally preferable in new code.
- Delegate `BeginInvoke`/`EndInvoke` depends on .NET Framework-era remoting behavior and is unsupported on modern .NET.
- `WebClient`/`WebRequest` examples should become `HttpClient` in new code.
- AppDomain-based isolation is not the general isolation model of modern .NET; use processes, containers, or appropriate load contexts.
- Asynchronous pipelines can use `Channel<T>` rather than blocking-only producer/consumer designs.
- Dedicated `System.Threading.Lock` is the current general-purpose lock option for C# 13+ / .NET 9+ while classic monitor locking remains supported.

The enduring lessons are races, shared-state discipline, signaling, atomicity, progress, and the cost of parallel execution. Modern APIs express those concepts with better composition, cancellation, and asynchronous behavior.

## 40. HTTP and network clients

Modern code should prefer `HttpClient` over legacy `WebRequest` and `WebClient`. Client lifetime affects connection pooling.

```csharp
using var client = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(5)
};

using HttpResponseMessage response = await client.GetAsync(
    "https://example.com/",
    HttpCompletionOption.ResponseHeadersRead);

response.EnsureSuccessStatusCode();

await using Stream body = await response.Content.ReadAsStreamAsync();
Console.WriteLine(response.StatusCode);
```

Server applications can use `IHttpClientFactory` to manage handlers and policy composition.

## 41. Reflection, attributes, and metadata

Reflection discovers types and members at runtime. It is powerful but more expensive than direct calls, and dynamic access must be considered in trimming and Native AOT scenarios.

```csharp
[AttributeUsage(AttributeTargets.Class)]
public sealed class HandlerAttribute(string name) : Attribute
{
    public string Name { get; } = name;
}

[Handler("telemetry")]
public sealed class TelemetryHandler;

Type type = typeof(TelemetryHandler);
HandlerAttribute? attribute = type
    .GetCustomAttributes(typeof(HandlerAttribute), inherit: false)
    .Cast<HandlerAttribute>()
    .SingleOrDefault();

Console.WriteLine(attribute?.Name);
```

Source generators or compile-time registration can replace reflection in designs that require predictable startup or AOT compatibility.

## 42. Unsafe code, pointers, and `stackalloc`

C# supports unsafe pointer operations when required, but unsafe code narrows the guarantees provided by managed execution.

```csharp
static int SumStackAllocated()
{
    Span<int> values = stackalloc int[4] { 3, 5, 7, 11 };
    int sum = 0;

    foreach (int value in values)
    {
        sum += value;
    }

    return sum;
}

Console.WriteLine(SumStackAllocated());
```

This example demonstrates stack allocation without pointer syntax. Direct pointer use should be confined to measured interop or performance requirements.

## 43. Operator overloads, indexers, and conversions

Operator overloads should model operations that are unsurprising in the problem domain.

```csharp
public readonly record struct Vector2(double X, double Y)
{
    public static Vector2 operator +(Vector2 left, Vector2 right)
        => new(left.X + right.X, left.Y + right.Y);

    public double this[int index] => index switch
    {
        0 => X,
        1 => Y,
        _ => throw new IndexOutOfRangeException()
    };
}

Vector2 v = new(1, 2) + new Vector2(3, 4);
Console.WriteLine($"{v[0]}, {v[1]}");
```

Implicit conversions should not hide data loss or significant work. Risky conversions should be explicit.

## 44. XML documentation and API contracts

XML documentation can feed IDE tooling and generated API documentation. It should describe contracts and boundaries rather than restating the code.

```csharp
/// <summary>Returns a ratio normalized to the 0..1 interval.</summary>
/// <exception cref="ArgumentOutOfRangeException">The value is outside 0..100.</exception>
public static double NormalizePercent(int value)
{
    if (value is < 0 or > 100)
    {
        throw new ArgumentOutOfRangeException(nameof(value));
    }

    return value / 100.0;
}
```

## 45. A compact recursive-descent parser

Recursive-descent parsing is useful for small rule languages, filters, and configuration expressions as well as compiler exercises.

The following parser preserves multiplication precedence over addition:

```csharp
public sealed class ExpressionParser
{
    private readonly string _text;
    private int _index;

    public ExpressionParser(string text)
    {
        _text = text;
    }

    public double Parse()
    {
        double value = ParseExpression();
        SkipSpaces();

        if (_index != _text.Length)
        {
            throw new FormatException($"Unexpected character: {_text[_index]}");
        }

        return value;
    }

    private double ParseExpression()
    {
        double value = ParseTerm();

        while (true)
        {
            SkipSpaces();

            if (Match('+'))
            {
                value += ParseTerm();
            }
            else if (Match('-'))
            {
                value -= ParseTerm();
            }
            else
            {
                return value;
            }
        }
    }

    private double ParseTerm()
    {
        double value = ParseNumber();

        while (true)
        {
            SkipSpaces();

            if (Match('*'))
            {
                value *= ParseNumber();
            }
            else if (Match('/'))
            {
                value /= ParseNumber();
            }
            else
            {
                return value;
            }
        }
    }

    private double ParseNumber()
    {
        SkipSpaces();
        int start = _index;

        while (_index < _text.Length &&
               (char.IsDigit(_text[_index]) || _text[_index] == '.'))
        {
            _index++;
        }

        if (start == _index ||
            !double.TryParse(
                _text.AsSpan(start, _index - start),
                System.Globalization.NumberStyles.Float,
                System.Globalization.CultureInfo.InvariantCulture,
                out double value))
        {
            throw new FormatException("A number was expected.");
        }

        return value;
    }

    private bool Match(char expected)
    {
        if (_index >= _text.Length || _text[_index] != expected)
        {
            return false;
        }

        _index++;
        return true;
    }

    private void SkipSpaces()
    {
        while (_index < _text.Length && char.IsWhiteSpace(_text[_index]))
        {
            _index++;
        }
    }
}

Console.WriteLine(new ExpressionParser("2 + 3 * 4").Parse());
```

A production parser also needs parentheses, unary operators, precise diagnostics, tokenization strategy, and resource limits.

## 46. Desktop, web, and application layers

Windows Forms and WPF remain Windows desktop frameworks. ASP.NET Core is the primary .NET web/API stack. .NET MAUI provides a shared model for mobile and desktop clients. The C# language itself is independent of these application frameworks.

Keeping domain rules out of UI event handlers improves testability and lets application logic survive a change in presentation technology.

## 47. Object contracts with `required`, `init`, and primary constructors

Modern C# can express construction contracts without relying solely on constructor overloads. An `init` accessor allows assignment during initialization, `required` makes member initialization a compiler-enforced creation contract, and primary constructors place constructor parameters on the class or struct declaration.

```csharp
public sealed class ServiceOptions(string endpoint)
{
    public string Endpoint { get; } =
        string.IsNullOrWhiteSpace(endpoint)
            ? throw new ArgumentException("Endpoint cannot be empty.", nameof(endpoint))
            : endpoint;

    public required TimeSpan Timeout { get; init; }
    public int RetryCount { get; init; } = 2;
}

var options = new ServiceOptions("https://api.example.com")
{
    Timeout = TimeSpan.FromSeconds(3)
};
```

`required` is not runtime validation; it strengthens initialization at compile time. A primary-constructor parameter referenced by instance members can be captured into object state. Storing the same value again in an explicit field can therefore create two copies unintentionally.

## 48. `ref struct`, `scoped`, and lifetime safety

`Span<T>` and `ReadOnlySpan<T>` are `ref struct` types. They cannot live arbitrarily on the managed heap; the compiler enforces rules that prevent references from outliving their targets.

```csharp
static int ParseHeader(scoped ReadOnlySpan<byte> data)
{
    if (data.Length < 4)
        throw new ArgumentException("Header must contain at least 4 bytes.", nameof(data));

    return data[0]
         | (data[1] << 8)
         | (data[2] << 16)
         | (data[3] << 24);
}
```

`scoped` helps express that a reference or `ref struct` value cannot escape the current call scope. `ref readonly` can pass or return large values by readonly reference without copying.

These features are best used when lifetime safety and copying costs are demonstrably important. Simpler by-value code should remain the default when it is adequate.

## 49. When to use `ValueTask`

`Task` and `Task<T>` should remain the default asynchronous return types. `ValueTask<T>` can be useful on hot paths where a large fraction of calls complete synchronously and measured `Task` allocations are significant.

```csharp
public sealed class LookupCache
{
    private readonly Dictionary<int, string> _cache = [];

    public ValueTask<string?> FindAsync(
        int id,
        CancellationToken cancellationToken = default)
    {
        if (_cache.TryGetValue(id, out string? value))
            return ValueTask.FromResult<string?>(value);

        return LoadSlowAsync(id, cancellationToken);
    }

    private static async ValueTask<string?> LoadSlowAsync(
        int id,
        CancellationToken cancellationToken)
    {
        await Task.Delay(20, cancellationToken);
        return id.ToString();
    }
}
```

A `ValueTask` has a stricter consumption model. Callers should normally await a returned instance directly and once. Multiple awaits, early result access, or mixing consumption techniques cannot be assumed safe. Without measured allocation pressure, `Task` is simpler.

## 50. Asynchronous streams with `IAsyncEnumerable<T>`

When an I/O source produces many values over time, `IAsyncEnumerable<T>` allows consumers to process values as they arrive instead of buffering the complete result.

```csharp
using System.Runtime.CompilerServices;

static async IAsyncEnumerable<int> ReadSamplesAsync(
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    for (int i = 0; i < 5; i++)
    {
        await Task.Delay(50, cancellationToken);
        yield return i * 10;
    }
}

await foreach (int sample in ReadSamplesAsync(cancellationToken))
{
    Console.WriteLine(sample);
}
```

`await foreach` composes asynchronous production and consumption. It does not automatically make every upstream source bounded; if the producer is backed by an unbounded queue, memory can still grow. High-volume systems may need a bounded `Channel<T>` or explicit capacity policy.

## 51. `Memory<T>`, `ArrayPool<T>`, and buffer ownership

`Span<T>` is ideal for short-lived stack-constrained views. Buffers that must survive asynchronous boundaries can be represented by `Memory<T>` or `ReadOnlyMemory<T>`.

When repeated large array allocation creates GC pressure, `ArrayPool<T>` can reuse buffers:

```csharp
using System.Buffers;

static int ReadPrefix(Stream stream)
{
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

    try
    {
        int read = stream.Read(buffer, 0, 4096);
        return read;
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(
            buffer,
            clearArray: true);
    }
}
```

A rented array can be larger than requested, so the logical data length must be tracked separately. Sensitive buffers may need clearing before return. Pooling can retain memory longer and should be driven by measured allocation pressure rather than used indiscriminately.

## 52. Garbage collection, large objects, and pinning

The .NET GC is generational. Most short-lived objects die in generation 0; survivors can be promoted to older generations. Large objects live on a separate large object heap. The default LOH threshold is 85,000 bytes and can be configured by the runtime.

Operational consequences include:

- heavy short-lived allocation causing frequent young-generation collections;
- repeated large temporary buffers increasing LOH and generation-2 pressure;
- long-lived pinning increasing heap fragmentation;
- unnecessary finalizers extending object lifetime.

Calling `GC.Collect()` more often is rarely the first performance fix. Reduce unnecessary allocations, shorten lifetimes, and avoid large transient objects first.

```csharp
static void Fill(Span<byte> destination)
{
    Random.Shared.NextBytes(destination);
}

byte[] buffer = new byte[16 * 1024];
Fill(buffer);
```

`Span<T>`, pooling, and reusable buffers can reduce allocation on hot paths, but they add ownership and lifetime complexity and should be justified by measurement.

## 53. Modeling time as a dependency with `TimeProvider`

Direct calls to `DateTime.UtcNow` and real delays make time-dependent business rules harder to test. `TimeProvider` lets applications treat time as a dependency.

```csharp
public sealed class ExpiringValue(TimeProvider timeProvider)
{
    private readonly TimeProvider _timeProvider = timeProvider;
    private DateTimeOffset _expiresAt;

    public void Reset(TimeSpan lifetime)
    {
        _expiresAt = _timeProvider.GetUtcNow() + lifetime;
    }

    public bool IsExpired()
    {
        return _timeProvider.GetUtcNow() >= _expiresAt;
    }
}
```

This makes expiration, retry windows, deadlines, and other temporal behavior deterministic under test and makes the dependency on time explicit.

## 54. High-throughput streaming I/O with `System.IO.Pipelines`

For network protocols, codecs, compression, or large streaming parsers, `System.IO.Pipelines` provides high-performance buffered I/O with explicit consumption and backpressure semantics.

```csharp
using System.IO.Pipelines;

static async Task<int> CountBytesAsync(
    PipeReader reader,
    CancellationToken cancellationToken)
{
    int total = 0;

    while (true)
    {
        ReadResult result = await reader.ReadAsync(cancellationToken);
        ReadOnlySequence<byte> buffer = result.Buffer;

        total += checked((int)buffer.Length);
        reader.AdvanceTo(buffer.End);

        if (result.IsCompleted)
            break;
    }

    await reader.CompleteAsync();
    return total;
}
```

Incorrect `AdvanceTo` boundaries can corrupt buffer-management semantics. Pipelines are a specialized performance tool; ordinary file and HTTP work is usually clearer through `Stream` and higher-level APIs.

## 55. Diagnosing ThreadPool starvation and blocking

Concurrency failures can appear as latency rather than incorrect results. Sync-over-async (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`), long lock waits, and blocking I/O can exhaust available ThreadPool workers.

.NET diagnostic tools can make these effects visible:

- `dotnet-counters` for live runtime counters;
- `dotnet-trace` for timed runtime and wait events;
- `dotnet-stack` or dump inspection for the call stacks holding workers.

A high thread count is a symptom, not the root cause. Increasing thread counts blindly can hide the problem temporarily. The useful question is which code is blocking workers. Keeping naturally asynchronous I/O asynchronous end to end is the primary correction for sync-over-async paths.

## 56. Current C# 14 and .NET 10 line

C# 14 adds extension members, null-conditional assignment, the `field` keyword, improved `Span<T>` conversions, more partial members, and user-defined compound assignment.

```csharp
public sealed class Job
{
    public string? Result { get; set; }
}

Job? current = new();
current?.Result = "done";

Console.WriteLine(current?.Result);
```

Null-conditional assignment is a C# 14 feature.

.NET 10 improves JIT inlining, devirtualization, stack allocation, Native AOT, and code generation. Runtime improvements still need workload-specific measurement.

## 57. Modernizing older C#/.NET code

Older teaching material remains useful for language fundamentals, but API choices and runtime assumptions should be updated.

- Prefer `List<T>` to `ArrayList` and `Dictionary<TKey,TValue>` to `Hashtable`.
- Prefer `HttpClient` to `WebRequest` and `WebClient`.
- Replace `Thread.Abort`, `Suspend`, and `Resume` with cooperative cancellation and task-based workflows.
- Prefer `IDisposable`, `IAsyncDisposable`, and `SafeHandle` to finalizer-centric resource ownership.
- Evaluate allocation behavior in string, I/O, and collection-heavy paths.
- Enable nullable reference analysis and analyzers for new code.
- Revisit reflection-heavy designs when trimming or Native AOT is required.
- Use unsafe code only for measured and constrained requirements.

## 58. Performance and production behavior

The cost of a C# application is not described by algorithmic complexity alone. Allocation rate, GC generations, boxing, interface dispatch, closure allocation, async state machines, contention, syscalls, I/O batching, and data layout can all affect latency.

Useful production questions include:

- Does the hot path allocate on every call?
- Can collection capacity be estimated?
- Does a LINQ pipeline create avoidable intermediate materialization?
- Is a `Task` created for genuine asynchronous waiting?
- Does code perform I/O while holding a shared lock?
- Are timeout and cancellation propagated end to end?
- Does serialization create unnecessary intermediate strings or byte arrays?
- Has the result been validated with a profiler or representative benchmark?

Language features should be selected with readability and maintenance cost as well as allocation, latency, and concurrency behavior in mind.

## Related courses

For a comparison of type systems and runtimes, see [Programming Languages](/en/programming-languages). For lower-level memory models, see [C Programming](/en/c-programming-fundamentals) and [C++ Programming](/en/object-oriented-programming-with-cpp). For a JVM comparison, see [Java Programming](/en/java-programming). For software design, see [Software Engineering](/en/software-engineering-process-requirements-design-quality).

## Naming progress guarantees correctly in lock-free algorithms

Using `Interlocked` does not automatically make an algorithm wait-free. **Lock-free** means the system as a whole continues to make progress; **wait-free** is stronger and guarantees completion for each operation after a bounded number of its own steps. **Obstruction-free** guarantees progress when an operation eventually runs in isolation.

A heavily contended compare-and-swap loop can repeatedly fail for one thread while other threads continue, so starvation is still possible.

Choosing between locks and lock-free structures should consider contention, fairness, proof complexity, and tail latency rather than only uncontended microbenchmarks.

## References

- Microsoft. *C# Guide*. https://learn.microsoft.com/dotnet/csharp/
- Microsoft. *C# Language Reference*. https://learn.microsoft.com/dotnet/csharp/language-reference/
- Microsoft. *What's new in C# 14*. https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14
- Microsoft. *What's new in .NET 10*. https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10/
- Microsoft. *.NET API Browser*. https://learn.microsoft.com/dotnet/api/
- ECMA International. *ECMA-334: C# Language Specification, 7th Edition*. https://ecma-international.org/publications-and-standards/standards/ecma-334/
- ECMA International. *ECMA-335: Common Language Infrastructure*. https://ecma-international.org/publications-and-standards/standards/ecma-335/
- Jeffrey Richter. *CLR via C#*, 4th Edition. Microsoft Press, 2012.
- Microsoft. *Managed Threading Best Practices*. https://learn.microsoft.com/dotnet/standard/threading/managed-threading-best-practices
- Microsoft. *Threading Objects and Features*. https://learn.microsoft.com/dotnet/standard/threading/threading-objects-and-features
- Microsoft. *The lock statement*. https://learn.microsoft.com/dotnet/csharp/language-reference/statements/lock
- Microsoft. *System.Threading.Channels library*. https://learn.microsoft.com/dotnet/core/extensions/channels
- Microsoft. *Asynchronous programming with async and await*. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/
- Joseph Albahari. *Threading in C#*. Updated 2011-04-27. https://www.albahari.com/threading/
- Microsoft. *Primary constructors*. https://learn.microsoft.com/dotnet/csharp/whats-new/tutorials/primary-constructors
- Microsoft. *ref struct types*. https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/ref-struct
- Microsoft. *Asynchronous programming scenarios*. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/async-scenarios
- Microsoft. *Generate and consume async streams*. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/generate-consume-asynchronous-stream
- Microsoft. *Memory<T> and Span<T> usage guidelines*. https://learn.microsoft.com/dotnet/standard/memory-and-spans/memory-t-usage-guidelines
- Microsoft. *.NET garbage collection*. https://learn.microsoft.com/dotnet/standard/garbage-collection/
- Microsoft. *Large object heap*. https://learn.microsoft.com/dotnet/standard/garbage-collection/large-object-heap
- Microsoft. *TimeProvider overview*. https://learn.microsoft.com/dotnet/standard/datetime/timeprovider-overview
- Microsoft. *System.IO.Pipelines*. https://learn.microsoft.com/dotnet/standard/io/pipelines
- Microsoft. *Debug ThreadPool starvation*. https://learn.microsoft.com/dotnet/core/diagnostics/debug-threadpool-starvation

## Cite This Work

Köker, M. A. (2015). C# Programming. alikoker.com.tr. https://alikoker.com.tr/en/csharp-programming

- BibTeX: https://alikoker.com.tr/en/csharp-programming.bib
- RIS: https://alikoker.com.tr/en/csharp-programming.ris
- CSL-JSON: https://alikoker.com.tr/en/csharp-programming.csl.json
