C# Programming
C# 14 and .NET 10 course notes covering the type system, object-oriented design, generics, collections, LINQ, delegates/events, async/await, concurrency, file and network I/O, reflection, memory, and performance with examples.
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.
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:
dotnet new console -n Sample
dotnet build Sample
dotnet run --project SampleThe 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>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.
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.
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.
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.
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>.
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.
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.
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.
10. 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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, tasks, and cancellation
Asynchrony is not parallelism. It is primarily useful for waiting on I/O without blocking an operating-system thread.
static async Task<string[]> FetchAllAsync(HttpClient client, Uri[] addresses, CancellationToken cancellationToken)
{
Task<string>[] tasks = addresses
.Select(uri => client.GetStringAsync(uri, cancellationToken))
.ToArray();
return await Task.WhenAll(tasks);
}Unlimited concurrency can overwhelm sockets, a remote service, or memory. Apply explicit concurrency limits when the workload can fan out.
22. Concurrency and shared state
Shared mutable state is a primary source of race conditions. lock protects a critical section, Interlocked handles simple atomic operations, and Channel<T> can model producer-consumer flows.
public sealed class Counter
{
private long _value;
public long Increment()
{
return Interlocked.Increment(ref _value);
}
public long Value => Interlocked.Read(ref _value);
}Legacy Thread.Suspend, Thread.Resume, and Thread.Abort patterns are not the modern .NET concurrency model. Use cooperative cancellation, tasks, and purpose-built synchronization primitives.
23. 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.
24. 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.
25. 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.
26. 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.
27. 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;
}28. 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.
29. 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.
30. 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.
31. 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>toArrayListandDictionary<TKey,TValue>toHashtable. - Prefer
HttpClienttoWebRequestandWebClient. - Replace
Thread.Abort,Suspend, andResumewith cooperative cancellation and task-based workflows. - Prefer
IDisposable,IAsyncDisposable, andSafeHandleto 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.
32. 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
Taskcreated 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. 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
- 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.