C# Programming

C# Programming
QR code for this page

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.

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.

The focus is 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.

Unit 1: .NET Execution Model, Types, and Control Flow

1.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.

.cs source
   |
   v
C# compiler
   |
   v
IL + metadata
   |
   v
CLR loading / verification
   |
   +--> JIT --------> native code
   |
   +--> AOT path ---> native image

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.

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.

1.2. Build, SDK, and Project Model

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

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.

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

1.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.

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.

1.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.

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.

1.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.

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.

1.6. Control Flow and Pattern Matching

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

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.

1.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.

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>.

1.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.

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.

Unit 2: Object Model, Collections, and Language Abstractions

Object modeling starts by identifying protected state and the operations allowed to modify it. Immutable data carriers and entities with identity and lifecycle have different design needs. Select collections according to lookup, ordering, and concurrent-access patterns. After defining equality and hash behavior, check which fields may change while an object remains in a collection.

2.1. 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.

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:

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.

2.2. Records, Equality, and Immutability

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

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.

2.3. 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.

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.

2.4. 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.

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.

2.5. Arrays and Span<T>

Span slicing sharing memory with the array so that a value 99 written through a nested slice appears in the original array while a copy allocates an independent new array
Span slices share memory

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.

backing memory
+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+
        ^-----------^
          Span<T>

Span<T> does not own the data; it is a bounded view over existing memory.
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.

2.6. 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.

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.

2.7. 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.

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.

2.8. LINQ and Deferred Execution

LINQ deferred execution showing the buffering OrderBy operator against streaming Where and Select
LINQ deferred execution

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

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.

2.9. 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.

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.

2.10. 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.

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.

Unit 3: Resource Management, Asynchrony, and Concurrency

An asynchronous pipeline begins by defining resource ownership and cleanup under cancellation. Distinguish wait time from genuinely parallel work; sending unbounded tasks to one resource may create queues rather than throughput. Make cancellation, timeouts, and error propagation explicit at each boundary. Assess pending work and tail latency alongside completed-task count.

3.1. Resource Ownership: IDisposable And IAsyncDisposable

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

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.

3.2. 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.

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.

3.3. async/await, Task, And Ownership of Work

C sharp async await flow separating the task I O wait and continuation
Async await continuation

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.

caller
   |
   v
async method ----> Task
   |
 await incomplete operation
   |
   +--> return control to caller
   |
I/O completes
   |
   v
continuation / state machine -> remaining code

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.

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.

3.4. 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.

3.5. 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.

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.

3.6. 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.

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.

3.7. 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.

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.

3.8. Interlocked, Volatile, And Memory Visibility

Scientific animation of program order and synchronization establishing a happens-before relation
Happens-before memory model

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

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:

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.

3.9. 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.

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.

3.10. Deadlock, Starvation, Livelock, and Contention

Circular wait in a deadlock wait-for graph
Deadlock cycle

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

A classic deadlock pattern is inconsistent lock ordering:

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.

3.11. Limiting Concurrency with SemaphoreSlim

Eight asynchronous tasks limited to three at a time by SemaphoreSlim with 3 permits, finishing at time 8 with a mean concurrency of 2.5
Concurrency limit 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.

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.

3.12. 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.

3.13. 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.

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.

3.14. Producer/consumer Pipelines, Channel<T>, And Backpressure

Producer bounded queue consumer and backpressure relationship
Bounded queue and backpressure

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

producers
  |  |  |
  v  v  v
+-----------+
| bounded   |
| Channel<T>|
+-----------+
  |  |  |
  v  v  v
consumers

production rate > consumption rate
        -> wait / reject / drop / bound capacity

A bounded channel introduces backpressure when producers outrun consumers:

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.

3.15. 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:

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.

3.16. Cancellation, Timeout, Deadline, and Graceful Shutdown

Cooperative cancellation and observation delay with computed steps and state transitions
Cooperative cancellation and observation delay

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.

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.

3.17. 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.

3.18. 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.

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.

3.19. 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.

double[] input = CreateInput();

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

PLINQ can parallelize some in-memory query pipelines:

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.

3.20. 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.

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.

Unit 4: Networking, Reflection, Low-Level Features, and Application Layers

4.1. 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.

4.2. HTTP and Network Clients

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

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.

4.3. 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.

[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.

4.4. Unsafe Code, Pointers, and stackalloc

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

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.

4.5. Operator Overloads, Indexers, and Conversions

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

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.

4.6. 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.

/// <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;
}

4.7. 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:

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.

4.8. 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.

4.9. Query Shape and Object Lifetime in Data Access

An ORM query costs more than its SQL execution time. Returned row count, network payload, client-side object allocation, and change-tracking overhead must be considered together. Projecting only the required columns, for example into a compact list-view result instead of a full entity, reduces transfer and retained state. The projection, however, does not automatically retain the update semantics of a tracked entity.

Loading object graphs without an explicit plan can cause N+1 queries or join amplification. Fetching two collection relationships in a single joined result may multiply the number of rows unexpectedly. Projection, separate queries, and explicit loading strategies should therefore be compared using observed cardinalities. Building an expression over IQueryable<T> does not guarantee server-side execution: the provider must be able to translate that expression. Crossing into IEnumerable<T> prematurely may shift filtering to the client.

A connection and its query result have distinct lifetimes. An open reader, deferred enumeration, or transaction scope can retain resources after the surrounding method appears to have completed. Asynchronous APIs free a waiting thread; they do not increase the physical capacity of the connection pool. Measurements should report query count, row count, transferred bytes, allocations, and connection occupancy. LINQ readability and actual data-access behavior are separate engineering properties.

4.10. Memory and Resource Lifetimes in Streaming Serialization

Materializing an entire database result set into objects before writing JSON or XML can make heap usage grow with result size. Forward-only reading through DbDataReader with incremental output reduces intermediate allocations, but ties the lifetime of the database connection and reader to completion of the output stream.

Design requirements include deterministic result ordering, NULL and type conversion, XML character handling or JSON escaping, client disconnects, and resource release after cancellation. Low heap use under many concurrent long-lived streams does not guarantee connection-pool health. Measurements include allocation volume, connection hold time, downstream backpressure, and output validity on failure.

Unit 5: Modern C#/.NET: Lifetimes, Memory, and High Performance

5.1. 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.

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.

5.2. 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.

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.

5.3. 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.

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.

5.4. 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.

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.

5.5. 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:

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.

5.6. 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.

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.

5.7. 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.

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.

5.8. 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.

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.

5.9. 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.

5.10. 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.

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.

5.11. 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.

Unit 6: Production Performance, Progress Guarantees, and Verification

6.1. 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.

6.2. 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.

6.3. Verifying .NET Behavior by Version and Measurement

C# language features should be separated from .NET runtime behavior. The language specification, BCL API contracts, and CoreCLR/JIT optimizations are different layers; one JIT output is not a language guarantee.

Scheduling in asynchronous and concurrent code can be nondeterministic. A test passing several times does not prove the absence of races. Cancellation, ownership, lock ordering, and shared-state contracts should remain explicit.

BenchmarkDotNet-style harnesses, dotnet-counters, dotnet-trace, and profilers answer different performance questions. Results are meaningful only with the .NET version, GC mode, and hardware context.

6.4. Shared State and Progress in C# Concurrency

Correct multicore programming is more than the absence of a data race. In C# and .NET concurrency, the consistency of observable operations, progress under lock or Interlocked-based synchronization, throughput loss under contention, and visibility effects of memory barriers should be evaluated separately; the label thread-safe does not specify all of these properties. Linearizability provides a strong correctness model, while lock-free and wait-free progress classes show why latency cannot be explained merely by counting mutexes.

  • Separate task-based asynchrony from actual parallel CPU execution.
  • Design cancellation and timeout behavior together with shared-state invariants.
  • Profile contention as well as correctness when using concurrent collections.

6.5. Reading Runtime Concepts Together

Treating C# features only as language syntax hides part of their production cost. async/await, Task, thread-pool scheduling, allocation, and reflection all consume resources inside the same runtime. Concepts such as Just-In-Time Compilation, Garbage Collection, Thread Pool, Lock-Free, Reflection, and Backpressure provide a useful frame for comparing C#/.NET behavior.

Under high concurrency, more parallel work does not automatically mean higher throughput. Thread-pool queues, GC pressure, lock contention, and downstream capacity have to be measured together. A language feature becomes an engineering decision only when its runtime behavior is included in the analysis.

Unit 7: CLR Type Contracts, Modern Interfaces, and the LINQ Execution Model

.NET interview material often combines CLR/CTS, assemblies, boxing, interfaces, and LINQ even though these belong to different layers.

7.1. CTS versus CLS

The Common Type System (CTS) defines how types are declared and managed by the CLR, including the value-type/reference-type model.

The Common Language Specification (CLS) is a narrower set of rules intended to improve interoperability across .NET languages.

CTS:
  runtime type system

CLS:
  cross-language public-contract subset

7.2. Namespace, Assembly, Module, and Metadata

A namespace is a naming/organization concept rather than a physical deployment unit.

An assembly can contain IL, metadata, a manifest, type/member definitions, references, and resources.

An assembly can contain one or more modules, although typical modern C# output uses one main module.

Reflection operates over this metadata.

7.3. GAC in Historical Context

The Global Assembly Cache is an important .NET Framework-era deployment concept for shared strong-named assemblies.

Modern .NET application deployment does not normally use the GAC as its dependency model. NuGet, runtime resolution, application-local dependencies, and framework-dependent or self-contained deployment represent a different model.

GAC therefore remains relevant when maintaining .NET Framework systems, not as the default architecture of modern .NET.

7.4. Boxing and Unboxing

Boxing a C# int into a 24-byte heap object that stays independent of the original and the memory difference between 4 KB and 32 KB for 1000 elements
Boxing and unboxing cost

Converting a value type to object or an implemented interface can box the value.

int n = 42;
object boxed = n;
int x = (int)boxed;

Boxing can allocate and copy the value. Generic collections avoid many boxing scenarios for value types.

Unboxing is not a numeric conversion; the boxed runtime type must be compatible with the target type.

7.5. Generic Variance

Selected generic interfaces and delegates can declare:

out T -> covariance
in T  -> contravariance

Covariance supports safe producer-oriented substitution; contravariance supports safe consumer-oriented substitution.

Variance is permitted only where the language can preserve type safety.

7.6. Modern Interface Members

The historical statement that a C# interface can never contain implementation is no longer generally true.

Modern interfaces can contain default instance implementations and static abstract or static virtual members.

Static abstract members support designs such as generic math, where a type-level operation becomes part of a generic contract.

These features are tools for contract evolution and generic abstraction, not unrestricted multiple implementation inheritance.

7.7. Partial Types and Extension Methods

A partial declaration splits one type's source definition across files. The compiler combines the pieces into one runtime type.

An extension method provides instance-like call syntax without modifying the target type or changing inheritance.

7.8. dynamic

dynamic defers selected type-binding operations from compile time to the runtime binder.

This can be useful at interop or genuinely dynamic boundaries, but excessive use moves errors from compile time to runtime and weakens refactoring guarantees.

7.9. IEnumerable<T> Versus IQueryable<T>

C# LINQ IEnumerable versus IQueryable execution and expression tree translation

LINQ over IEnumerable<T> executes .NET delegates over a sequence.

IQueryable<T> represents a query through expression trees and a query provider:

IEnumerable<T>
 -> delegate / local execution

IQueryable<T>
 -> expression tree
 -> provider translation
 -> remote/native query

A database provider may translate some C# expressions to SQL and reject or reshape others.

Materialization with ToList() or a transition to AsEnumerable() therefore changes where later operators run.

7.10. A Query Provider Is a Compiler Boundary

Code can be valid C# while still being unsupported or inefficient for a particular remote query provider.

The distinction between language validity and provider translation is fundamental to LINQ/ORM performance.

For a comparison of type systems and runtimes, see Programming Languages. For lower-level memory models, see C Programming and C++ Programming. For a JVM comparison, see Java Programming. For software design, see Software Engineering.

References

  • Brendan Gregg. Systems Performance: Enterprise and the Cloud. Pearson Education, 2014.
  • 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.
  • Joseph Albahari. Threading in C#. Updated 2011-04-27. https://www.albahari.com/threading/
  • Maurice Herlihy; Nir Shavit. The Art of Multiprocessor Programming. Morgan Kaufmann, 2008.
  • Microsoft. .NET API Browser. https://learn.microsoft.com/dotnet/api/
  • Microsoft. .NET garbage collection. https://learn.microsoft.com/dotnet/standard/garbage-collection/
  • Microsoft. Asynchronous programming scenarios. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/async-scenarios
  • Microsoft. Asynchronous programming with async and await. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/
  • Microsoft. C# Guide. https://learn.microsoft.com/dotnet/csharp/
  • Microsoft. C# Language Reference. https://learn.microsoft.com/dotnet/csharp/language-reference/
  • Microsoft. Common Type System. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/standard/base-types/common-type-system
  • Microsoft. Debug ThreadPool starvation. https://learn.microsoft.com/dotnet/core/diagnostics/debug-threadpool-starvation
  • Microsoft. Generate and consume async streams. https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/generate-consume-asynchronous-stream
  • Microsoft. Interfaces — C# Language Specification. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/interfaces
  • Microsoft. Large object heap. https://learn.microsoft.com/dotnet/standard/garbage-collection/large-object-heap
  • Microsoft. Managed Threading Best Practices. https://learn.microsoft.com/dotnet/standard/threading/managed-threading-best-practices
  • Microsoft. Memory&lt;T&gt; and Span&lt;T&gt; usage guidelines. https://learn.microsoft.com/dotnet/standard/memory-and-spans/memory-t-usage-guidelines
  • 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. Standard Query Operators Overview. Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/csharp/linq/standard-query-operators/
  • Microsoft. System.IO.Pipelines. https://learn.microsoft.com/dotnet/standard/io/pipelines
  • Microsoft. System.Threading.Channels library. https://learn.microsoft.com/dotnet/core/extensions/channels
  • Microsoft. The lock statement. https://learn.microsoft.com/dotnet/csharp/language-reference/statements/lock
  • Microsoft. Threading Objects and Features. https://learn.microsoft.com/dotnet/standard/threading/threading-objects-and-features
  • Microsoft. TimeProvider overview. https://learn.microsoft.com/dotnet/standard/datetime/timeprovider-overview
  • Microsoft. What's new in .NET 10. https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10/
  • Microsoft. What's new in C# 14. https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14
Contents