C# 8
This release is often the most important starting point when modernizing an older codebase.
Back to overview
Section titled “Back to overview”Nullable reference types
Section titled “Nullable reference types”Nullable reference types let the compiler distinguish between values that are expected to be present and values that may legitimately be absent.
#nullable enable
public string FormatName(string firstName, string? middleName, string lastName){ return middleName is null ? $"{firstName} {lastName}" : $"{firstName} {middleName} {lastName}";}Legacy code often relied on convention alone:
public string FormatName(string firstName, string middleName, string lastName){ return middleName == null ? firstName + " " + lastName : firstName + " " + middleName + " " + lastName;}The newer form makes null intent explicit and gives the compiler room to help.
Switch expressions
Section titled “Switch expressions”Switch expressions turn multi-branch assignment logic into a compact expression.
var label = order.Status switch{ OrderStatus.Created => "Created", OrderStatus.Processing => "Processing", OrderStatus.Completed => "Completed", _ => "Unknown"};A common legacy equivalent was a switch statement with repeated assignment:
string label;switch (order.Status){ case OrderStatus.Created: label = "Created"; break; case OrderStatus.Processing: label = "Processing"; break; case OrderStatus.Completed: label = "Completed"; break; default: label = "Unknown"; break;}Property patterns
Section titled “Property patterns”Property patterns are useful when the decision depends on object state rather than only on a single enum or scalar.
var kind = request switch{ { IsAuthenticated: false } => "Anonymous", { User.IsAdmin: true } => "Administrator", _ => "RegularUser"};This can replace nested null checks and property access chains that are harder to scan.
Using declarations
Section titled “Using declarations”Using declarations reduce indentation when a disposable resource should live for the rest of the current scope.
using var stream = File.OpenRead(path);using var reader = new StreamReader(stream);
var content = reader.ReadToEnd();Legacy style:
using (var stream = File.OpenRead(path))using (var reader = new StreamReader(stream)){ var content = reader.ReadToEnd();}Indices and ranges
Section titled “Indices and ranges”Indices and ranges provide built-in syntax for slicing arrays and spans.
var firstThree = values[..3];var middle = values[2..5];var last = values[^1];Legacy code often used LINQ for simple slicing:
var middle = values.Skip(2).Take(3).ToArray();For arrays and spans, the newer syntax is more direct and often better suited to performance-sensitive code.
Async streams
Section titled “Async streams”Async streams make it possible to consume asynchronous sequences with await foreach.
await foreach (var item in repository.StreamItemsAsync(cancellationToken)){ Console.WriteLine(item);}This can replace patterns where an entire result set had to be buffered before iteration started.