The C# You Actually Use
C# has grown a huge surface area, but the working MVC developer leans on a small, stable core
every day: classes, properties, nullability discipline, and string handling. This
lesson covers that daily-driver subset as it looks on the job today: ASP.NET Core on .NET 8+,
with C# 12 as the floor β not the ceiling. Real enterprise portfolios are mixed: older
services sit on C# 12 while newer ones ride the latest compiler (C# 13 brought params
collections and a dedicated Lock type; C# 14 added the field keyword, extension members,
and null-conditional assignment). The habit that serves you: write shared code to the floor,
adopt the newest features freely where a project's target framework allows.
Types, classes, and records
Most of your types will be plain classes with auto-properties: public string Name { get; set; }.
In MVC apps these show up as three flavors that look alike but have different jobs β entities
(EF-mapped, mutable), ViewModels (shaped for one screen), and DTOs (shaped for one API
call). Keeping them separate is the single most important habit in this course; AutoMapper exists
to move data between them.
Records give you value-based equality and concise immutable types:
public record CustomerDto(int Id, string Name);
They're the default choice for DTOs and lookup results. Three more modern idioms worth making
reflexes: file-scoped namespaces (namespace Orders.Web; β one line, no indent pyramid),
primary constructors (C# 12 lets classes take constructor parameters in the declaration β
public class OrdersController(IOrderService orders) β which erases most DI boilerplate), and
collection expressions (int[] ids = [1, 2, 3];). You'll see all three constantly in
current codebases and samples.
Nullability: let the compiler carry it
New .NET projects enable nullable reference types (<Nullable>enable</Nullable>) by default:
string means "never null" and string? means "might be null," enforced by compiler warnings.
Lean on it β annotate honestly instead of silencing warnings with !. The defensive operators
still matter at the edges: ?. (null-conditional), ?? (null-coalescing), and guard
clauses at method entry. The pragmatic rule: treat anything that crossed a boundary (model
binding, database, session, config) as possibly null until you've checked it. Model binding in
particular will happily hand your action a null property. Legacy note: if you ever touch an old
.NET Framework codebase, assume nothing is annotated and everything can be null.
var, strings, and everyday idioms
Use var when the type is obvious from the right-hand side (var list = new List<Order>();)
and spell the type out when it isn't β especially with LINQ queries whose element type matters.
This is a readability decision, not a performance one. Pattern matching (if (result is Order { Status: "Open" } order)) is the modern replacement for cast-and-check chains.
For strings: string interpolation ($"Order {id} saved") beats string.Format and +
concatenation for readability. Use StringBuilder only in real loops building large text.
Compare with string.Equals(a, b, StringComparison.OrdinalIgnoreCase) rather than .ToLower()
β it's faster and culture-safe, which matters when your app runs on a server with a different
locale than your laptop. And prefer string.IsNullOrWhiteSpace() as the standard "did the user
actually type something" check; it handles null, empty, and spaces in one call.
Everything else β LINQ, async, DI β gets its own lesson next.