Modern C# Features
Definition
Modern C# refers to features introduced from C# 8 onwards that improve expressiveness, safety, and performance. Each version brings incremental enhancements that reduce boilerplate, enable new patterns, and bring C# closer to a "low-ceremony" language while maintaining backward compatibility.
C# 8 Features
Nullable Reference Types
See Nullable Types for full coverage. Enables compile-time null safety:
#nullable enable
string name = "Alice"; // Non-nullable
string? nick = null; // Nullable
Switch Expressions
See Pattern Matching. Expression-based switch with pattern matching:
string Grade(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
_ => "F"
};
Async Streams (IAsyncEnumerable<T>)
Asynchronous iteration — yield items as they become available:
async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = new StreamReader(path);
while (await reader.ReadLineAsync() is { } line)
{
yield return line;
}
}
// Consume with await foreach
await foreach (var line in ReadLinesAsync("data.csv"))
{
Console.WriteLine(line);
}
Indices and Ranges
New syntax for indexing and slicing:
int[] numbers = { 0, 1, 2, 3, 4, 5 };
int last = numbers[^1]; // 5 — from end
int[] slice = numbers[1..4]; // [1, 2, 3] — exclusive end
int[] lastTwo = numbers[^2..]; // [4, 5]
int[] all = numbers[..]; // copy of entire array
Using Declarations
No more nesting blocks for disposal:
using var connection = new SqlConnection(connectionString);
using var command = connection.CreateCommand();
// Dispose called automatically at end of scope
Default Interface Members
Interfaces can define method implementations:
public interface ILogger
{
void Log(string message);
void LogWarning(string message) => Log($"[WARN] {message}");
void LogError(string message) => Log($"[ERROR] {message}");
}
C# 9 Features
Records
Immutable reference types with value-based equality:
public record Person(string FirstName, string LastName, int Age);
var p1 = new Person("Alice", "Smith", 30);
var p2 = new Person("Alice", "Smith", 30);
Console.WriteLine(p1 == p2); // True — value equality
var p3 = p1 with { Age = 31 }; // Non-destructive mutation
Init-Only Setters
Properties that can only be set during object initialization:
public class Configuration
{
public string Host { get; init; } = "localhost";
public int Port { get; init; } = 5432;
}
var config = new Configuration { Host = "db.example.com", Port = 3306 };
// config.Host = "other"; // Compile error — init-only
Top-Level Statements
Remove boilerplate for simple programs:
// Program.cs — the entire file
using Microsoft.AspNetCore.Builder;
var app = WebApplication.CreateBuilder().Build();
app.MapGet("/", () => "Hello, World!");
app.Run();
Target-Typed New
Let the compiler infer the type from context:
Person person = new("Alice", "Smith", 30);
List<int> numbers = new() { 1, 2, 3 };
Dictionary<string, int> map = new() { ["a"] = 1 };
Covariant Return Types
Override methods with a more specific return type:
public abstract class Repository
{
public abstract Entity GetById(int id);
}
public class SqlRepository : Repository
{
public override SqlEntity GetById(int id) => // More specific return type
_context.SqlEntities.Find(id);
}
C# 10 Features
Global Usings
Define usings once for the entire project:
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
<!-- Or auto-generate in .csproj -->
<ImplicitUsings>enable</ImplicitUsings>
File-Scoped Namespaces
Reduce indentation — one of the most impactful quality-of-life improvements:
namespace MyApp.Services; // No braces needed
public class UserService
{
// All types in this file belong to MyApp.Services
}
Record Structs
Value-type records with value-based equality:
public record struct Point(double X, double Y);
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a == b); // True
Constant Interpolated Strings
const string Prefix = "ERROR_";
const string ErrorCode = $"{Prefix}NOT_FOUND"; // Compile-time constant
Extended Property Patterns
Simplified nested property matching:
// C# 9 — nested braces required
if (order is { Customer: { IsVip: true } }) { }
// C# 10 — dot notation
if (order is { Customer.IsVip: true }) { }
C# 11 Features
Raw String Literals
Multi-line strings without escaping — ideal for JSON, SQL, regex:
var json = """
{
"name": "Alice",
"age": 30,
"regex": "C:\\Users\\.*"
}
""";
var query = """
SELECT u.Id, u.Name
FROM Users u
WHERE u.Active = 1
""";
List Patterns
Match elements in arrays and lists:
int[] numbers = { 1, 2, 3, 4, 5 };
var message = numbers switch
{
[] => "Empty",
[var single] => $"One: {single}",
[var a, var b] => $"Two: {a}, {b}",
[var first, .., var last] => $"First: {first}, Last: {last}",
};
Required Members
Force callers to initialize specific properties:
public class User
{
public required string Email { get; init; }
public required string Name { get; init; }
public string? Phone { get; init; }
}
var user = new User { Email = "a@b.com", Name = "Alice" }; // OK
// var bad = new User { Name = "Alice" }; // Error — Email is required
Generic Math Support
Use arithmetic operators in generic code:
T Add<T>(T a, T b) where T : INumber<T> => a + b;
T Sum<T>(params T[] values) where T : INumber<T> =>
values.Aggregate(T.Zero, (acc, v) => acc + v);
Auto-Default Structs
Fields in structs are auto-initialized to their default values:
public struct Measurement
{
public double Value; // Auto-initialized to 0.0
public string Unit; // Auto-initialized to null
}
File-Scoped Types
Types visible only within the file they are declared in:
file class Helper { /* Only visible in this file */ }
C# 12 Features
Primary Constructors for Classes
Parameters declared on the class itself — available throughout:
public class ProductService(IRepository<Product> repo, ILogger<ProductService> logger)
{
public Product? Find(int id)
{
logger.LogInformation("Finding product {Id}", id);
return repo.Find(id);
}
}
Unlike records, primary constructor parameters in classes are not automatically exposed as properties. They are captured into the class body. If you need a public property, declare it explicitly:
public class UserController(ILogger logger)
{
public ILogger Logger { get; } = logger; // Explicit property
}
Collection Expressions
Unified syntax for initializing collections:
int[] array = [1, 2, 3];
List<string> list = ["a", "b", "c"];
Span<int> span = [10, 20, 30];
ReadOnlySpan<char> ros = ['h', 'e', 'l', 'l', 'o'];
// Spread elements from another collection
int[] all = [..array, 4, 5]; // [1, 2, 3, 4, 5]
Inline Arrays
High-performance fixed-size arrays without heap allocation:
[System.Runtime.CompilerServices.InlineArray(4)]
public struct FourInts
{
private int _element0;
}
var buffer = new FourInts();
buffer[0] = 10;
buffer[1] = 20;
Alias Any Type
Use using aliases for any type, including tuples and nullable types:
using Point = (double X, double Y);
using UserId = int?;
C# 13 Features
Params Collections
params now works with ReadOnlySpan<T> and List<T>, not just arrays:
void Log(ReadOnlySpan<string> messages)
{
foreach (var msg in messages)
Console.WriteLine(msg);
}
Log("Starting", "Processing", "Done"); // No array allocation
New Lock Object
System.Threading.Lock replaces lock(obj) with better performance:
private readonly Lock _lock = new();
using (_lock.EnterScope())
{
// Critical section — faster than monitor-based lock
}
Partial Properties
Separate declaration from implementation (useful for source generators):
public partial string ConnectionString { get; }
// In generated file:
public partial string ConnectionString => _config.GetConnectionString();
Field Keyword
Access the backing field from a property accessor without declaring it explicitly:
public string Name
{
get => field;
set => field = value?.Trim() ?? throw new ArgumentNullException();
}
Extension Types (Preview)
A new approach to extending types without modifying them:
extension StringExtensions for string
{
public bool IsNullOrEmpty => string.IsNullOrEmpty(this);
public string Capitalize => char.ToUpper(this[0]) + this[1..];
}
Migration Guidance
Adopt modern features incrementally:
- Low risk, high impact first — file-scoped namespaces, global usings, target-typed
new,usingdeclarations - Add to new code — records, init setters, switch expressions, pattern matching
- Enable per file — nullable reference types (
#nullable enable), primary constructors - Project-wide changes last —
ImplicitUsings,Nullablein .csproj
<!-- Recommended .csproj settings for modern C# -->
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Common Pitfalls
Top-level statements are great for simple programs and minimal APIs, but they can make large applications harder to navigate. Once a Program.cs grows beyond a few dozen lines, extract classes and methods.
Enabling <Nullable>enable</Nullable> project-wide on legacy code produces hundreds of warnings. Migrate file by file with #nullable enable and fix warnings before making it a project-level setting. Do not set <TreatWarningsAsErrors> for nullable warnings until migration is complete.
Use record for data-centric types where value equality makes sense (DTOs, value objects, events). Use class for types with identity-based equality (entities, services). Use record struct for small value types.
Key Takeaways
- Each C# version adds features that reduce boilerplate and improve safety — adopt incrementally.
- Records provide immutable, value-equal data types with minimal syntax.
- File-scoped namespaces and global usings dramatically reduce noise in every file.
- Nullable reference types catch null bugs at compile time — enable them.
- Pattern matching and switch expressions replace verbose branching.
- Primary constructors (C# 12) simplify dependency injection in services.
- Use
LangVersionset tolatestto always have access to new features.
Interview Questions
Q: What's new in C# 10? C# 10 introduced global usings, file-scoped namespaces, record structs, constant interpolated strings, extended property patterns, and improved lambda type inference. These are primarily quality-of-life improvements that reduce boilerplate.
Q: What's new in C# 11?
C# 11 added raw string literals, list patterns, required members, generic math support (INumber<T>), file-scoped types, and auto-default structs. Raw string literals and required members have the most day-to-day impact.
Q: What's new in C# 12?
C# 12 brought primary constructors for classes, collection expressions ([1, 2, 3]), inline arrays, alias any type, and optional params with ReadOnlySpan. Primary constructors and collection expressions are the most widely adopted.
Q: What are records?
Records are reference types (or struct types with record struct) that provide value-based equality, non-destructive mutation (with expressions), and built-in formatting. They are ideal for DTOs, value objects, and event types.
Q: What are top-level statements?
Top-level statements let you write a C# program without a Program class or Main method. The compiler generates the Main method automatically. They are ideal for minimal APIs and simple console programs.
Q: What is the difference between init and set?
init allows a property to be set only during object initialization (constructor or object initializer). After initialization, the property is effectively read-only. set allows the property to be modified at any time. Use init for immutable configuration objects and DTOs.