C#
C# started as Javaβs Microsoft counterpart and then grew into something more interesting. LINQ brought functional data manipulation into the mainstream before most languages had it. async/await appeared in C# 5 in 2012 β years before JavaScript got it. Nullable reference types make null safety optional but enforced across an entire codebase. The language has been genuinely innovative, not just catching up.
The .NET ecosystemβs main drawback used to be Windows lock-in. Thatβs gone β .NET 8+ is cross-platform, fast, and the ASP.NET Core performance numbers are consistently among the best in web framework benchmarks.
π’ Junior
Value Types vs Reference Types
The distinction is fundamental to how C# manages memory.
Value types (structs, primitives, enums) live on the stack or inline within their containing object. Assignment copies the value.
Reference types (classes, interfaces, delegates, arrays, strings) store a reference. Assignment copies the reference β both variables point to the same object.
int a = 5;
int b = a; // copy
b = 10; // a is still 5
var p1 = new Person("Alice");
var p2 = p1; // same object
p2.Name = "Bob";
Console.WriteLine(p1.Name); // "Bob" β same reference
string is a reference type but immutable and treated specially β string operations always return new strings. This is why string.Concat in a loop is O(nΒ²): use StringBuilder.
Properties and Auto-Properties
public class User {
public int Id { get; init; } // settable only in constructor/initializer
public string Name { get; set; } = string.Empty;
public string Email { get; private set; } = string.Empty;
private int _age;
public int Age {
get => _age;
set {
if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
_age = value;
}
}
}
var user = new User { Id = 1, Name = "Alice" }; // object initializer
// user.Id = 2; // Error β init-only
LINQ
Language Integrated Query provides a unified query syntax over any IEnumerable<T>.
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evens = numbers.Where(n => n % 2 == 0);
var squares = numbers.Select(n => n * n);
var sum = numbers.Where(n => n > 5).Sum();
// Grouping
var grouped = people
.GroupBy(p => p.Department)
.Select(g => new { Dept = g.Key, Count = g.Count() });
// Query syntax (sugar over method chaining)
var result = from p in people
where p.Age > 25
orderby p.Name
select new { p.Name, p.Email };
LINQ is lazy β the query is not executed until enumerated. Call .ToList() or .ToArray() to materialize and avoid multiple enumerations.
async / await
The async/await model in C# uses Task and Task<T> as the async abstraction. The runtime handles thread scheduling via the thread pool.
public async Task<User> GetUserAsync(int id) {
var response = await _http.GetAsync($"/api/users/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<User>()
?? throw new InvalidOperationException("Null response");
}
// Parallel execution
var tasks = ids.Select(id => GetUserAsync(id));
var users = await Task.WhenAll(tasks);
Never use .Result or .Wait() in async code β it blocks the thread and can cause deadlocks in environments with a single-threaded synchronization context (e.g., old ASP.NET, WinForms).
π‘ Medior
Generics and Constraints
public class Repository<T> where T : class, IEntity, new() {
private readonly List<T> _store = new();
public void Add(T item) => _store.Add(item);
public T? FindById(int id) => _store.FirstOrDefault(x => x.Id == id);
public IEnumerable<T> GetAll() => _store.AsReadOnly();
}
// Constraints:
// where T : class β reference type
// where T : struct β value type
// where T : new() β has parameterless constructor
// where T : SomeBaseClass β inherits from
// where T : ISomeInterface β implements
Generic methods:
public static TResult Map<TInput, TResult>(TInput input, Func<TInput, TResult> mapper)
=> mapper(input);
Delegates, Events, and Lambdas
A delegate is a type-safe function pointer. Action<T>, Func<T, TResult>, and Predicate<T> are built-in generic delegates.
Func<int, int, int> add = (a, b) => a + b;
add(3, 4); // 7
Action<string> log = msg => Console.WriteLine($"[LOG] {msg}");
// Events β multicast delegates with publisher/subscriber semantics
public class Button {
public event EventHandler<ClickArgs>? Clicked;
protected virtual void OnClicked(ClickArgs e) => Clicked?.Invoke(this, e);
public void Click() => OnClicked(new ClickArgs { X = 0, Y = 0 });
}
var btn = new Button();
btn.Clicked += (sender, e) => Console.WriteLine($"Clicked at {e.X},{e.Y}");
Pattern Matching
C# pattern matching makes switch expressions powerful and exhaustive-checked.
string Classify(object obj) => obj switch {
null => "null",
int n when n < 0 => "negative int",
int n => $"positive int: {n}",
string { Length: 0 } => "empty string",
string s => $"string: {s}",
IEnumerable<int> list => $"int list with {list.Count()} items",
_ => "unknown"
};
// Deconstruction in switch
var result = point switch {
(0, 0) => "origin",
(var x, 0) => $"on x-axis at {x}",
(0, var y) => $"on y-axis at {y}",
var (x, y) => $"at ({x}, {y})"
};
Records
Records are reference types with value-based equality and immutability by default. They are ideal for DTOs and data transfer.
public record User(int Id, string Name, string Email);
var u1 = new User(1, "Alice", "alice@example.com");
var u2 = u1 with { Name = "Bob" }; // non-destructive mutation β new object
u1 == u2; // false (different Name)
var u3 = new User(1, "Alice", "alice@example.com");
u1 == u3; // true (value equality, not reference equality)
record struct is a value-type record β stack-allocated, no reference sharing.
π΄ Senior
Span<T> and Memory
Span<T> is a stack-only type that represents a contiguous region of memory β slice of an array, stackallocated memory, or unmanaged memory β without copying.
void ProcessData(ReadOnlySpan<byte> data) {
var header = data[..4]; // first 4 bytes β no allocation
var body = data[4..]; // rest β no allocation
var magic = MemoryMarshal.Read<int>(header);
}
// Zero-allocation string parsing
ReadOnlySpan<char> csv = "Alice,30,admin".AsSpan();
var name = csv[..csv.IndexOf(',')]; // span slice β no string allocation
Memory<T> is the heap-compatible counterpart (usable across await). ArrayPool<T>.Shared.Rent() reuses buffers from a pool to avoid GC pressure in high-throughput paths.
Expression Trees
Expression trees represent code as data β the AST at runtime. LINQ to SQL, EF Core, and mocking frameworks use them to translate C# lambdas into SQL queries or other representations.
Expression<Func<User, bool>> filter = u => u.Age > 25 && u.Name.StartsWith("A");
// Entity Framework compiles this to SQL at runtime:
// WHERE Age > 25 AND Name LIKE 'A%'
context.Users.Where(filter).ToList();
// You can build expressions dynamically:
var param = Expression.Parameter(typeof(User), "u");
var body = Expression.AndAlso(
Expression.GreaterThan(
Expression.Property(param, "Age"),
Expression.Constant(25)
),
Expression.Call(
Expression.Property(param, "Name"),
typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!,
Expression.Constant("A")
)
);
var lambda = Expression.Lambda<Func<User, bool>>(body, param);
CLR Memory Model and GC Generations
The .NET GC uses generational collection. Objects start in Gen 0. Survivors promote to Gen 1, then Gen 2. Large objects (β₯85 KB) go directly to the LOH (Large Object Heap), which is only collected during Gen 2 collection.
GC pauses are the main latency source in server applications. Mitigation strategies:
Use ArrayPool<T> and MemoryPool<T> to reuse large arrays instead of creating them per-request.
Avoid finalizers β they delay GC reclamation by moving objects to the finalizer queue. Use IDisposable + Dispose() with using instead.
Use struct for small, short-lived objects that are heavily allocated β they live on the stack or inline and have zero GC overhead.
System.Runtime.GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency switches to a mode that avoids Gen 2 collections during a critical time window β useful for real-time scenarios.
Nullable Reference Types
Enable with <Nullable>enable</Nullable> in .csproj. The compiler tracks nullability and warns at potential NullReferenceException sites.
string? nullable = null; // may be null
string nonNull = "hello"; // must not be null
nullable?.ToUpper(); // OK β null-conditional
nonNull.ToUpper(); // OK
// nullable.ToUpper(); // Warning: dereference of possibly null
string name = nullable ?? "default"; // null-coalescing
string upper = nullable!.ToUpper(); // null-forgiving β you assert it's not null
The ! (null-forgiving) operator suppresses the warning β treat it like as in TypeScript. Use it only at verified boundaries where you know better than the compiler.
Senior Gotchas
Task.Run should not be used to expose async APIs over synchronous code β it just burns a thread pool thread. Wrap existing sync-over-async or redesign as truly async.
Closures in LINQ and async code capture variables by reference. Modifying the variable after the lambda is created changes what the lambda sees β the classic loop variable capture bug. Use var captured = i before the lambda.
IEnumerable<T> has no Count property β calling .Count() (extension method) iterates the sequence. This is O(n) for lazy sequences. If you call Count() frequently, materialize with .ToList() first.
Disposing an object twice is usually safe (if Dispose is idempotent), but accessing a disposed object is not. ObjectDisposedException is the result. Use using statements to scope object lifetimes so disposal happens at exactly one well-defined point.
Value types (structs) implement interfaces through boxing β wrapping them in a heap-allocated object. Boxing is subtle and expensive in hot paths. Use generic constraints (where T : IMyInterface) to avoid boxing.