Skip to content

C# 8

This release is often the most important starting point when modernizing an older codebase.

Modernization Overview

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 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 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 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 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 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.