C# Evolved

See how C# has evolved — then move through curated guides, browse paths, and related-feature trails that keep discovery moving.

C# 2.0
string message = string.Format(
    "Hello, {0}! You have {1} messages.",
    name, count);
C# 6.0+
string message =
    $"Hello, {name}! You have {count} messages.";

Start here

Begin with cornerstone guides that help teams move from familiar C# into the latest, secure, and modern language patterns.

Async Streams

C# 8.0 NETCore 3.0

Async streams pair IAsyncEnumerable<T> with await foreach so asynchronous producers can yield values progressively instead of buffering everything first.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public sealed class PriceService
{
    public async IAsyncEnumerable<decimal> ReadPricesAsync()
    {
        foreach (var price in new[] { 9.99m, 11.25m, 10.5m })
        {
            await Task.Delay(20);
            yield return price;
        }
    }
}

public static class Program
{
    public static async Task Main()
    {
        var service = new PriceService();

        await foreach (decimal price in service.ReadPricesAsync())
        {
            Console.WriteLine($"Streamed price: {price:F2}");
        }
    }
}

With expressions

C# 9.0 .NET 5.0

With expressions let you copy immutable data and update only what changed, keeping your intent crisp and your models predictable.

using System;

public record DeploymentConfig(string Service, string Region, int Replicas, bool DryRun);

public static class Program
{
    public static void Main()
    {
        var baseline = new DeploymentConfig("orders-api", "eastus", 2, DryRun: true);

        var productionPlan = baseline with
        {
            Replicas = 4,
            DryRun = false
        };

        Console.WriteLine($"Baseline:   {baseline}");
        Console.WriteLine($"Production: {productionPlan}");
    }
}

nameof + CallerArgumentExpression

C# 10.0 .NET 6.0

nameof and CallerArgumentExpression help your APIs report exactly what went wrong, with refactor-safe names and call-site expressions.

using System;
using System.Runtime.CompilerServices;

public static class Guard
{
    public static void That(
        bool condition,
        string message,
        [CallerArgumentExpression("condition")] string? conditionExpression = null)
    {
        if (!condition)
        {
            throw new ArgumentException(
                $"{message} (Condition: {conditionExpression})",
                nameof(condition));
        }
    }
}

public static class Program
{
    public static void Main()
    {
        var quantity = -2;

        try
        {
            Guard.That(quantity > 0, "Quantity must be positive");
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine($"Param: {ex.ParamName}");
        }
    }
}

LINQ

C# 3.0 NETFx 3.5

Start with readable query pipelines when you want data-heavy code to stay direct and expressive.

var people = new[]
{
    new { Age = 30, LastName = "Smith", FullName = "Alice Smith" },
    new { Age = 16, LastName = "Jones", FullName = "Bob Jones" },
    new { Age = 25, LastName = "Brown", FullName = "Carol Brown" },
};

var adults = people
    .Where(person => person.Age >= 18)
    .OrderBy(person => person.LastName)
    .Select(person => person.FullName);

Async and await

C# 5.0 NETFx 4.5

Learn the async workflow most teams reach for first when modern apps need responsive I/O without callback sprawl.

public class ProfileService
{
    public async Task<string> LoadProfileAsync(HttpClient client, string id)
    {
        var response = await client.GetAsync($"/profiles/{id}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

Nullable reference types

C# 8.0 NETCore 3.0

Use compiler-backed null-safety to make modern codebases more secure, intentional, and easier to reason about.

string? FindDisplayName() => null;

string? displayName = FindDisplayName();

if (displayName is null)
{
    return;
}

Console.WriteLine(displayName.Length);