C#
Microsoft's object-oriented language and the foundation of the whole .NET ecosystem.
C# is the language I have spent most of my career in. These are the notes I keep coming back to — the parts that bite, not the tutorial.
The split between value and reference types explains most surprises in C#.
// record: reference type, value equality, immutable by default
public record User(string Name, string Email);
// record struct: value type with the same equality semantics
public readonly record struct Money(decimal Amount, string Currency);
var a = new User("Ada", "ada@example.com");
var b = new User("Ada", "ada@example.com");
Console.WriteLine(a == b); // True — records compare by valueNullable reference types turn "this could be null" into something the compiler checks. They are a compile-time promise, not a runtime guarantee: data coming from JSON or a database can still arrive null.
#nullable enable
string? maybe = null; // allowed
string sure = maybe; // warning CS8600async is about not blocking threads, not about going faster. An async method
that never awaits anything real just adds overhead.
// Wrong: sequential, one after the other
foreach (var id in ids)
results.Add(await client.GetAsync(id));
// Right: all in flight at once
var results = await Task.WhenAll(ids.Select(client.GetAsync));Always take a CancellationToken and pass it down. A request the user
abandoned should not keep working.
public async Task<Order> GetAsync(int id, CancellationToken cancellationToken)
{
return await _db.Orders
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken)
?? throw new NotFoundException(id);
}Never use
.Resultor.Wait()on a Task. In any context with a synchronisation context it deadlocks, and the stack trace tells you nothing.
LINQ queries are deferred: nothing runs until you enumerate. That is a feature until you enumerate the same query three times and hit the database three times.
var query = db.Users.Where(u => u.IsActive); // no SQL yet
var count = query.Count(); // SQL #1
var first = query.First(); // SQL #2
var all = query.ToList(); // SQL #3With Entity Framework, the line between what runs in SQL and what runs in
memory matters. AsEnumerable() pulls everything down before filtering.
| Method | Empty collection | No match |
|---|---|---|
First() |
throws | throws |
FirstOrDefault() |
null |
null |
Single() |
throws | throws, and also if there are two |
Pattern matching is what finally killed most of my long if chains.
public decimal Price(Shipment shipment) => shipment switch
{
{ Weight: > 100 } => 50m,
{ Express: true, Weight: var w } => 10m + w * 0.5m,
{ Destination: "ES" } => 5m,
_ => 12m,
};stringcomparison: useStringComparison.Ordinalunless you genuinely want culture-aware behaviour. The default is culture-sensitive and surprises you the day someone runs it under a Turkish locale.IDisposableinsideasync: it isawait using, notusing.- Struct mutation through an interface silently operates on a boxed copy.
DateTime.Nowis machine local time. StoreDateTimeOffset.UtcNow.