Chapter 23: Modern C# Features (C# 9–12+)
Today we’re on Chapter 23: Modern C# Features (C# 9–12+) – this is the chapter where we finally get to see how beautiful, clean, and expressive modern C# has become!
Starting from C# 9 (2020), Microsoft added a ton of features that make writing code much shorter, safer, more readable, and more fun. Many of these features are now considered best practice in 2026, and most new projects use them by default.
We’re going to cover the most important modern features you’ll see and use every day:
- Records (immutable data classes)
- Init-only properties (set only at creation)
- Top-level statements (no Main method!)
- Pattern matching enhancements (super powerful!)
- Source generators (magic code generation)
I’m going to explain everything very slowly, step by step, with tons of real-life examples, before & after comparisons, and practical mini-projects — just like we’re sitting together in Hyderabad looking at the same screen. Let’s dive in! 🚀
1. Records – Immutable Data Classes (C# 9+)
Records are the modern way to create simple data classes (like DTOs, value objects, data transfer objects).
Why records are awesome:
- Immutable by default (great for thread safety & functional programming)
- Value equality (two records are equal if all properties are equal – not just reference!)
- With-expressions (create a copy with one field changed – super clean)
- ToString(), Equals(), GetHashCode() are automatically generated
- Much shorter syntax
Old way (C# 8 or earlier):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Person { public string Name { get; } public int Age { get; } public Person(string name, int age) { Name = name; Age = age; } public override bool Equals(object obj) { ... } // Painful! public override int GetHashCode() { ... } public override string ToString() { ... } } |
Modern way with record:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
record Person(string Name, int Age); // One line! // Or with body if needed record Person(string Name, int Age) { public string Greeting => $"Hi, I'm {Name}, {Age} years old!"; } |
Usage – value equality & with-expression:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Person p1 = new Person("Webliance", 25); Person p2 = new Person("Webliance", 25); Console.WriteLine(p1 == p2); // True! (value equality) Console.WriteLine(p1.Equals(p2)); // True // Create a modified copy (immutable!) Person older = p1 with { Age = 26 }; Console.WriteLine(older); // Person { Name = Webliance, Age = 26 } Console.WriteLine(p1); // Still 25! Original unchanged |
Positional records vs nominal records:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Positional (short & beautiful) record Point(int X, int Y); // Nominal (more explicit) record Person { public string Name { get; init; } public int Age { get; init; } } |
2. Init-only Properties – Set Only at Creation (C# 9+)
Init-only properties can be set only during object initialization – perfect for immutable objects.
Old way (read-only):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
class Book { private readonly string _title; public string Title => _title; public Book(string title) { _title = title; } } |
Modern way with init:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Book { public string Title { get; init; } // Can only be set during creation public decimal Price { get; init; } public DateTime Published { get; init; } } // Usage Book book = new Book { Title = "Clean Code", Price = 999.99m, Published = new DateTime(2008, 8, 1) }; // book.Title = "New Title"; // ERROR! Init-only – cannot change later |
Records automatically make primary constructor parameters init-only!
3. Top-level Statements – No More Boilerplate (C# 9+)
Old way (classic Program.cs):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; namespace MyApp { class Program { static void Main(string[] args) { Console.WriteLine("Hello, World!"); } } } |
Modern way (C# 9+ – top-level statements):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// Program.cs – that's ALL! Console.WriteLine("Hello, World! 🌍"); Console.WriteLine($"Current time: {DateTime.Now:dd-MMM-yyyy HH:mm}"); // You can use args too! if (args.Length > 0) Console.WriteLine($"First argument: {args[0]}"); |
Even better (C# 10+): You can have implicit usings and global usings – no need to write using System; everywhere!
File: GlobalUsings.cs (create this file)
|
0 1 2 3 4 5 6 7 8 |
global using System; global using System.Collections.Generic; global using System.Linq; |
Now your Program.cs becomes super clean!
4. Pattern Matching Enhancements – Super Powerful (C# 9–12+)
Pattern matching lets you check types and values in a very expressive way.
Old way:
|
0 1 2 3 4 5 6 7 8 9 |
if (obj is Person p && p.Age > 18) { Console.WriteLine($"{p.Name} is an adult!"); } |
Modern patterns (C# 9–12):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
object value = new Person("Rahul", 22); // 1. Type pattern + property pattern if (value is Person { Age: > 18 } adult) { Console.WriteLine($"{adult.Name} is an adult!"); } // 2. Relational patterns int score = 85; string grade = score switch { >= 90 => "A", >= 80 => "B", >= 70 => "C", _ => "F" }; // 3. Logical patterns (and, or, not) if (value is Person p and { Age: >= 18 and <= 65 }) { Console.WriteLine($"{p.Name} is working age."); } // 4. List patterns (C# 11+) int[] numbers = [1, 2, 3, 4, 5]; if (numbers is [1, .., 5]) // Starts with 1, ends with 5 { Console.WriteLine("Starts with 1 and ends with 5!"); } |
5. Source Generators – Magic Code Generation (C# 9+)
Source generators are like Roslyn-based macros – they automatically generate code at compile time.
Most famous examples:
- System.Text.Json source generator (fast JSON serialization)
- Microsoft.Extensions.Options validation
- AutoMapper source generator
- Mediator pattern generators
Real example (simple one):
Many libraries use source generators so you don’t write boilerplate.
Example – JSON source generator (recommended in 2026):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// In your project file (.csproj) <ItemGroup> <JsonSerializerContext Include="PersonContext" /> </ItemGroup> // Context class [JsonSerializable(typeof(Person))] [JsonSourceGenerationOptions(WriteIndented = true)] public partial class PersonContext : JsonSerializerContext { } // Usage – super fast & AOT compatible string json = JsonSerializer.Serialize(person, PersonContext.Default.Person); Person loaded = JsonSerializer.Deserialize(json, PersonContext.Default.Person); |
Mini-Project: Modern Person Manager with Records & Top-Level Statements
Program.cs (modern style – no class, no Main!)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// GlobalUsings.cs (separate file) global using System; global using System.Collections.Generic; // Person.cs record Person(string Name, int Age, string City = "Hyderabad"); // Main program var people = new List<Person> { new("Rahul", 22), new("Priya", 21), new("Sneha", 23, "Delhi") }; // Pattern matching foreach (var person in people) { string message = person switch { { Age: < 18 } => $"{person.Name} is a minor.", { Age: >= 18 and <= 30 } p => $"{p.Name} is young and from {p.City}!", _ => $"{person.Name} is experienced." }; Console.WriteLine(message); } // With-expression var olderRahul = people[0] with { Age = 23 }; Console.WriteLine($"Updated: {olderRahul}"); // JSON serialization string json = JsonSerializer.Serialize(people, new JsonSerializerOptions { WriteIndented = true }); Console.WriteLine("\nJSON:\n" + json); |
Summary – What We Learned Today
- Records → short, immutable, value-equality data classes
- Init-only properties → set only during object creation
- Top-level statements → clean, minimal Program.cs
- Pattern matching → is, switch, relational, logical, list patterns
- Source generators → automatic code generation at compile time (fast JSON, etc.)
Your Homework (Super Practical!)
- Create a new console project called ModernCSharpMaster (use .NET 10!)
- Use records to create:
- Product (Name, Price, Category)
- Order (Id, CustomerName, List<Product> Items)
- Use top-level statements in Program.cs
- Use pattern matching to:
- Check if order total > ₹5000 (discount message)
- Classify products by price range
- Serialize the order to JSON and save to file
- Bonus: Use with-expression to create a discounted order
Next lesson: Best Practices & Design Patterns – we’re going to learn how to write professional, clean, maintainable C# code!
You’re doing absolutely fantastic! 🎉 Any modern feature confusing? Want more examples with records, patterns, or source generators? Just tell me — I’m right here for you! 💙
