You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
C# evolves rapidly with annual releases. This lesson covers the most impactful features from recent versions (C# 9 through C# 12) that every modern C# developer should know.
Records are immutable reference types with built-in value equality:
// Record declaration
public record Person(string Name, int Age);
// Usage
var alice = new Person("Alice", 30);
var alice2 = new Person("Alice", 30);
Console.WriteLine(alice == alice2); // True (value equality!)
Console.WriteLine(alice); // Person { Name = Alice, Age = 30 }
// Non-destructive mutation with 'with'
var older = alice with { Age = 31 };
Console.WriteLine(older); // Person { Name = Alice, Age = 31 }
| Type | Syntax | Value/Reference | Mutable |
|---|---|---|---|
record (or record class) | record Person(string Name); | Reference | Immutable by default |
record struct | record struct Point(int X, int Y); | Value | Mutable by default |
readonly record struct | readonly record struct Point(int X, int Y); | Value | Immutable |
Tip: Use records for data transfer objects (DTOs), domain models, and any type where value equality makes sense.
Write programs without the boilerplate of a class and Main method:
// Before (traditional)
namespace MyApp;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
// After (top-level statements)
Console.WriteLine("Hello, World!");
Top-level statements are ideal for small programs, scripts, and learning. Only one file per project can use top-level statements.
Declare using directives that apply to all files in a project:
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
Or in the .csproj file:
<ItemGroup>
<Using Include="System.Text.Json" />
</ItemGroup>
Tip: When
ImplicitUsingsis enabled in your project, common namespaces are automatically imported.
Reduce indentation by declaring the namespace without braces:
// Before
namespace MyApp.Services
{
public class UserService
{
// ...
}
}
// After
namespace MyApp.Services;
public class UserService
{
// ...
}
Define constructor parameters directly on the class or struct:
// Before
public class UserService
{
private readonly IUserRepository _repo;
private readonly ILogger<UserService> _logger;
public UserService(IUserRepository repo, ILogger<UserService> logger)
{
_repo = repo;
_logger = logger;
}
public async Task<User?> GetUserAsync(int id)
{
_logger.LogInformation("Getting user {Id}", id);
return await _repo.FindAsync(id);
}
}
// After (primary constructor)
public class UserService(IUserRepository repo, ILogger<UserService> logger)
{
public async Task<User?> GetUserAsync(int id)
{
logger.LogInformation("Getting user {Id}", id);
return await repo.FindAsync(id);
}
}
Tip: Primary constructor parameters are captured as fields. Be careful not to mutate them accidentally.
A unified syntax for creating collections:
// Arrays, lists, spans — all use the same syntax
int[] array = [1, 2, 3];
List<string> list = ["a", "b", "c"];
Span<int> span = [4, 5, 6];
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.