.NET developer: Full stack with Angular

Sefism.

Chapter 01

C# syntax you use every day

The parts of modern C# that show up in every ASP.NET Core codebase. C# 14 ships with .NET 10; most of what is here works from C# 9 onwards.

Value types and reference types

Also called struct, class, stack and heap

What
A struct (int, decimal, DateTime, your own structs) is copied when you assign or pass it. A class is a reference type: assigning it copies the reference, so two variables can point at one object.
Why
It explains why changing an object inside a method changes it for the caller, while changing an int does not, and why decimal is the type for money.
C#
var a = new Account { Balance = 100m };
var b = a;            // same object
b.Balance = 50m;      // a.Balance is now 50

int x = 1;
int y = x;            // a copy
y = 2;                // x is still 1

Watch out

Using double for money: 0.1 + 0.2 is not exactly 0.3. Use decimal for amounts.

Records

Also called record, record struct, with expression, DTO

What
A record is a type built for data: the compiler writes value equality, a readable ToString and, with positional syntax, an immutable constructor and properties. with makes a changed copy.
Why
Records are the natural shape for request and response DTOs, events and value objects, where two instances with the same data should be equal.
C#
public record OrderDto(int Id, decimal Total, string Status);

var a = new OrderDto(1, 250m, "Paid");
var b = a with { Status = "Refunded" };
Console.WriteLine(a == b);   // False: values differ

Watch out

Using records as EF Core entities: tracked entities need identity, not value equality, so keep entities as classes.

Pattern matching and switch expressions

Also called switch expression, is pattern, property pattern

What
Patterns test the shape of a value: its type, a constant, a range or its properties. A switch expression returns a value from the first arm that matches.
Why
It replaces long if/else chains with a compact, checked list of cases, and the compiler warns when a switch over an enum misses a value.
C#
decimal Fee(Payment p) => p switch
{
    { Method: "card", Amount: > 10_000m } => 0m,
    { Method: "card" } => p.Amount * 0.02m,
    BankTransfer => 25m,
    null => throw new ArgumentNullException(nameof(p)),
    _ => 0m,
};

Watch out

Putting the catch-all _ arm first, so the specific arms below it never run.

Nullable reference types

Also called NRT, string?, null-forgiving operator, <Nullable>enable

What
With <Nullable>enable</Nullable> (on in every new template) string means never null and string? means may be null. The compiler warns when you might dereference a null.
Why
NullReferenceException is the most common .NET crash. Treating these warnings as errors moves that crash from production to compile time.
C#
public User? FindByEmail(string email) =>
    _users.FirstOrDefault(u => u.Email == email);

var user = FindByEmail("a@b.com");
Console.WriteLine(user?.Name ?? "not found");
user!.Name = "x";   // ! silences the warning: avoid

Watch out

Sprinkling ! to make warnings disappear: it hides the bug instead of handling the null.

Primary constructors

Also called class primary constructor, C# 12

What
Since C# 12, a class can declare constructor parameters next to its name. The parameters are in scope in the whole class body.
Why
Services that take dependencies through DI become short: no field declarations and no assignment boilerplate.
C#
public class OrderService(AppDb db, ILogger<OrderService> log)
{
    public async Task<Order?> Get(int id)
    {
        log.LogInformation("Loading {OrderId}", id);
        return await db.Orders.FindAsync(id);
    }
}

Watch out

Assigning to the parameter inside a method: it is mutable and captured, so the change leaks into every later call.

Collection expressions

Also called [1, 2, 3], spread operator, C# 12

What
Since C# 12, [a, b, c] creates an array, List, Span or other collection, chosen by the target type. .. spreads another collection into it.
Why
One short syntax for every collection type, which the compiler can also optimise.
C#
int[] ids = [1, 2, 3];
List<string> roles = ["admin", "member"];
List<string> all = [..roles, "auditor"];
int[] none = [];

Watch out

Using var x = [1, 2];: there is no target type, so it does not compile.

Generics and constraints

Also called generic type, where T, List<T>

What
Generics let a class or method work with any type while staying type-safe: List<Order> only holds orders. Constraints such as where T : class limit which types are allowed.
Why
Collections, repositories, Task<T> and ILogger<T> are all generic. Reading generic signatures is a daily skill in .NET.
C#
public interface IRepository<T> where T : class, IEntity
{
    Task<T?> GetAsync(int id, CancellationToken ct);
    Task AddAsync(T item, CancellationToken ct);
}

T Max<T>(T a, T b) where T : IComparable<T> =>
    a.CompareTo(b) >= 0 ? a : b;

Watch out

Wrapping EF Core in a generic repository that hides IQueryable, then fetching whole tables to filter in memory.

Extension methods and extension members

Also called extension block, this parameter, C# 14

What
An extension method adds a method to a type you do not own, through a static class and a this parameter. C# 14 adds extension blocks, which can also declare extension properties.
Why
All of LINQ is extension methods on IEnumerable, and every builder.Services.AddX() call in Program.cs is one.
C#
public static class StringExtensions
{
    public static string Mask(this string card) =>
        card.Length < 4 ? "****" : $"**** {card[^4..]}";
}

Console.WriteLine("4111111111111111".Mask());
// **** 1111

Watch out

Hiding real business logic in extension methods, where it is hard to find and cannot be mocked.

Exceptions, using and finally

Also called try catch, throw, IDisposable, using declaration

What
Code throws an exception for a situation it cannot handle; the nearest matching catch up the call stack handles it. using disposes connections, streams and other resources even when an exception is thrown.
Why
Catch only what you can handle, let the rest reach one global handler, and never leak a connection or file handle.
C#
try
{
    await using var tx =
        await db.Database.BeginTransactionAsync();
    await Transfer(from, to, amount);
    await tx.CommitAsync();
}
catch (DbUpdateConcurrencyException)
{
    return Results.Conflict();
}

Watch out

Writing throw ex; instead of throw;, which resets the stack trace and hides where the error started.

01 · C# syntax you use every day1 / 11