C# feature map

Organized starting points for content that can grow into full feature guides.

Explore the feature map

Search by feature name, example topic, or newer capability, then keep moving with related guides on each feature page.

Filter features by version

Pick a version family and view either everything up to that version or only features added after it. Search also matches example topics and newer capability notes.

Anonymous types

C# 3.0 NETFx 3.5

Anonymous types let you create lightweight objects on the fly with `new { property = value }` syntax. The compiler generates a class for you with equality and `ToString()` built in.

Shaping data with anonymous types in LINQ

Anonymous types let you pluck just the fields you need from a query without defining a new class.

var result = new[] 
{ 
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 }
};

var linqProjection = result.Select(p => new { p.Name, UpperName = p.Name.ToUpper() });

Creating temporary objects inline

Use anonymous types to group related values for local use without ceremony.

var person = new { FirstName = "John", LastName = "Doe", Age = 28 };
var location = new { City = "Seattle", State = "WA", ZipCode = "98101" };

Console.WriteLine($"{person.FirstName} {person.LastName} lives in {location.City}, {location.State}");

Read full guide →

Async and await

C# 5.0 NETFx 4.5

`async` and `await` make asynchronous code read like straight-line code while still freeing threads during I/O waits.

Awaiting an I/O-bound operation

Async methods let you keep the thread free while waiting on I/O-heavy work like HTTP calls or file access.

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();
    }
}

Read full guide →

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.

From buffered lists to async streams

Async streams let callers process records as soon as each result arrives instead of waiting for an entire list to finish loading.

Before

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

public sealed class PriceService
{
    public async Task<List<decimal>> ReadPricesAsync()
    {
        var results = new List<decimal>();

        foreach (var price in new[] { 9.99m, 11.25m, 10.5m })
        {
            await Task.Delay(20);
            results.Add(price);
        }

        return results;
    }
}

public static class Program
{
    public static async Task Main()
    {
        var service = new PriceService();
        List<decimal> prices = await service.ReadPricesAsync();

        foreach (decimal price in prices)
        {
            Console.WriteLine($"Buffered price: {price:F2}");
        }
    }
}

After

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}");
        }
    }
}

Consuming streamed updates with await foreach

Use await foreach to process each asynchronous result in order while naturally composing async validation and filtering logic.

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

public sealed class SensorReading
{
    public SensorReading(DateTime timestampUtc, double temperatureCelsius)
    {
        TimestampUtc = timestampUtc;
        TemperatureCelsius = temperatureCelsius;
    }

    public DateTime TimestampUtc { get; }
    public double TemperatureCelsius { get; }
}

public static class SensorFeed
{
    public static async IAsyncEnumerable<SensorReading> ReadingsAsync()
    {
        var values = new[] { 21.1, 21.4, 22.0, 22.7 };

        foreach (double value in values)
        {
            await Task.Delay(15);
            yield return new SensorReading(DateTime.UtcNow, value);
        }
    }
}

public static class Program
{
    public static async Task Main()
    {
        await foreach (SensorReading reading in SensorFeed.ReadingsAsync())
        {
            if (reading.TemperatureCelsius >= 22.0)
            {
                Console.WriteLine($"Alert: {reading.TemperatureCelsius:F1}C at {reading.TimestampUtc:HH:mm:ss}");
            }
        }
    }
}

Read full guide →

Attributes and reflection

C# 1.0 NETFx 4.5

Attributes add metadata to code, and reflection reads that metadata back when you need lightweight discovery.

Discover handlers marked with a custom attribute

Attributes give you a simple opt-in marker, and reflection can build a registry once at startup.

using System;
using System.Collections.Generic;
using System.Reflection;

[AttributeUsage(AttributeTargets.Class)]
public sealed class JobHandlerAttribute : Attribute
{
    private readonly string _name;

    public JobHandlerAttribute(string name)
    {
        _name = name;
    }

    public string Name
    {
        get { return _name; }
    }
}

public interface IJobHandler
{
    void Run();
}

[JobHandler("nightly-report")]
public sealed class NightlyReportJob : IJobHandler
{
    public void Run()
    {
        Console.WriteLine("Generated the nightly report.");
    }
}

[JobHandler("rebuild-search-index")]
public sealed class SearchIndexJob : IJobHandler
{
    public void Run()
    {
        Console.WriteLine("Rebuilt the search index.");
    }
}

public static class Program
{
    public static void Main()
    {
        Dictionary<string, IJobHandler> handlers = new Dictionary<string, IJobHandler>();
        Type[] discoveredTypes = Assembly.GetExecutingAssembly().GetTypes();

        foreach (Type discoveredType in discoveredTypes)
        {
            if (!typeof(IJobHandler).IsAssignableFrom(discoveredType) || discoveredType.IsInterface || discoveredType.IsAbstract)
            {
                continue;
            }

            object[] attributes = discoveredType.GetCustomAttributes(typeof(JobHandlerAttribute), false);
            if (attributes.Length == 0)
            {
                continue;
            }

            JobHandlerAttribute attribute = (JobHandlerAttribute)attributes[0];
            handlers.Add(attribute.Name, (IJobHandler)Activator.CreateInstance(discoveredType));
        }

        handlers["nightly-report"].Run();
    }
}

Generate help text from property attributes

Reflection is often most useful when it turns static metadata into a small runtime experience such as docs, help, or validation messages.

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Reflection;

public sealed class UserSettings
{
    [Description("The time zone used when formatting dashboard timestamps.")]
    public string TimeZone { get; set; }

    [Description("When true, summaries are grouped into one daily email instead of immediate notifications.")]
    public bool DigestEmails { get; set; }
}

public static class Program
{
    public static void Main()
    {
        List<string> helpLines = new List<string>();
        PropertyInfo[] properties = typeof(UserSettings).GetProperties();

        foreach (PropertyInfo property in properties)
        {
            object[] attributes = property.GetCustomAttributes(typeof(DescriptionAttribute), false);
            if (attributes.Length == 0)
            {
                continue;
            }

            DescriptionAttribute description = (DescriptionAttribute)attributes[0];
            helpLines.Add(property.Name + ": " + description.Description);
        }

        Console.WriteLine(string.Join(Environment.NewLine, helpLines.ToArray()));
    }
}

Read full guide →

Auto-implemented properties

C# 3.0 NETFx 3.5

Auto-implemented properties let you declare a property with just `{ get; set; }`, letting the compiler generate the backing field for you. They cut property boilerplate while keeping your intent crystal clear.

Replacing backing fields with auto-properties

Auto-implemented properties eliminate boilerplate field declarations for simple getters and setters.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string City { get; set; }
}

Migrating from manual properties

See how auto-properties transform verbose property implementations into clean, minimal syntax.

// Before: Backing fields and explicit properties
public class PersonOld
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

// After: Auto-implemented properties
public class PersonNew
{
    public string Name { get; set; }
}

Read full guide →

Collection expressions

C# 12.0 .NET 8.0

Use a unified, concise syntax to initialize arrays, lists, spans, and other collections with cleaner code and optional spread operators for composition.

Unified syntax for array and list creation

Collection expressions provide a consistent syntax for creating arrays, lists, and other collections, making initialization cleaner and less verbose.

Before

// Array
int[] numbers = new int[] { 1, 2, 3, 4, 5 };

// List
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };

// Span
Span<int> values = new Span<int>(new int[] { 10, 20, 30 });

After

// Array
int[] numbers = [1, 2, 3, 4, 5];

// List
List<string> names = ["Alice", "Bob", "Charlie"];

// Span
Span<int> values = [10, 20, 30];

Spread operator for combining collections

The spread operator (`..`) makes it easy to combine existing collections into a new one without explicit loops or LINQ.

int[] first = [1, 2, 3];
int[] second = [4, 5];
int[] third = [6];

// Combine collections with spread operator
int[] combined = [..first, ..second, ..third];
// Result: [1, 2, 3, 4, 5, 6]

Console.WriteLine(string.Join(", ", combined));

Read full guide →

Default Interface Members

C# 8.0 NETCore 3.0

Evolve interfaces by adding members with default implementations so existing implementers do not break immediately.

Add default behavior to an interface

Default members let existing implementations keep working when an interface evolves.

using System;

public interface ITemperatureFormatter
{
    string FormatCelsius(double celsius) => celsius.ToString("0.0") + " C";
}

public class BasicFormatter : ITemperatureFormatter
{
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine(((ITemperatureFormatter)new BasicFormatter()).FormatCelsius(21.34));
    }
}

Override interface default behavior

Implementations can still provide custom behavior when needed.

using System;

public interface ITemperatureFormatter
{
    string FormatCelsius(double celsius) => celsius.ToString("0.0") + " C";
}

public class FancyFormatter : ITemperatureFormatter
{
    public string FormatCelsius(double celsius) => celsius.ToString("0.00") + " °C";
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine(((ITemperatureFormatter)new FancyFormatter()).FormatCelsius(21.34));
    }
}

Read full guide →

Exception filters

C# 6.0 NETFx 4.6

Exception filters let you catch only the failures you actually know how to handle.

Catch only the cases you can recover from

A `when` clause keeps the recovery path narrow instead of over-catching and branching inside the handler.

using System;

public sealed class ApiException : Exception
{
    public ApiException(int statusCode, string message) : base(message)
    {
        StatusCode = statusCode;
    }

    public int StatusCode { get; }
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine(ReadProfile());
    }

    private static string ReadProfile()
    {
        try
        {
            throw new ApiException(404, "Profile was not found.");
        }
        catch (ApiException ex) when (ex.StatusCode == 404)
        {
            return "Use cached profile until the user signs in again.";
        }
    }
}

Log during the filter and keep bubbling

Filters can record context before the runtime commits to a handler, letting the original exception continue upward unchanged.

using System;

public static class Program
{
    public static void Main()
    {
        try
        {
            try
            {
                throw new InvalidOperationException("Disk quota reached.");
            }
            catch (Exception ex) when (Log(ex, "nightly import"))
            {
                Console.WriteLine("This block never runs because the filter returned false.");
            }
        }
        catch (InvalidOperationException)
        {
            Console.WriteLine("The original exception keeps bubbling after logging.");
        }
    }

    private static bool Log(Exception ex, string operation)
    {
        Console.WriteLine(string.Format(
            "Logging {0}: {1} - {2}",
            operation,
            ex.GetType().Name,
            ex.Message));
        return false;
    }
}

Read full guide →

Expression-bodied members

C# 6.0 NETFx 4.6

Expression-bodied members let you define methods, properties, and accessors using arrow syntax (=>) instead of braces and return statements, reducing boilerplate and improving readability for simple logic.

Expression-bodied methods and properties

Replace statement bodies with a single expression using the => syntax.

// Before: Statement bodies with braces
public class PersonOld
{
    private string firstName;
    private string lastName;

    public string GetFullName()
    {
        return firstName + " " + lastName;
    }

    public bool IsAdult
    {
        get
        {
            return Age >= 18;
        }
    }

    public int Age
    {
        get
        {
            return DateTime.Now.Year - BirthYear;
        }
    }

    public int BirthYear { get; set; }
}

// After: Expression-bodied members
public class PersonNew
{
    private string firstName;
    private string lastName;

    public string GetFullName() => firstName + " " + lastName;

    public bool IsAdult => Age >= 18;

    public int Age => DateTime.Now.Year - BirthYear;

    public int BirthYear { get; set; }
}

Expression-bodied accessors and indexers

Use arrow syntax with getters, setters, and indexers.

public class AccessorsAndIndexers
{
    private string[] _items = new[] { "apple", "banana", "cherry" };
    private int _accessCount;

    // Expression-bodied auto-property getter
    public int ItemCount => _items.Length;

    // Expression-bodied custom getter
    public int AccessCount => _accessCount;

    // Expression-bodied setter (C# 6.0+)
    public int Value { get; set; }

    // Indexer with expression body
    public string this[int index]
    {
        get => _items[index];
        set => _items[index] = value;
    }

    // Indexer with computed result
    public char this[int index, int charIndex]
        => _items[index][charIndex];

    // Method with side effect (incrementing access count)
    public string GetItem(int index)
    {
        _accessCount++;
        return _items[index];
    }

    // Read-only property with LINQ
    public IEnumerable<string> SortedItems => _items.OrderBy(x => x);

    // Ternary expression in property
    public string FirstOrDefault => _items.Length > 0 ? _items[0] : "empty";
}

Expression-bodied constructors and operators

Define constructors and operators concisely with expressions.

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    // Expression-bodied constructor (C# 7.0+)
    public Point(double x, double y) => (X, Y) = (x, y);

    // Expression-bodied method
    public double Distance() => Math.Sqrt(X * X + Y * Y);

    // Expression-bodied operator overload
    public static Point operator +(Point a, Point b)
        => new Point(a.X + b.X, a.Y + b.Y);

    public static Point operator -(Point a, Point b)
        => new Point(a.X - b.X, a.Y - b.Y);

    public static Point operator *(Point p, double scalar)
        => new Point(p.X * scalar, p.Y * scalar);

    // Expression-bodied method for validation
    public bool IsOrigin => X == 0 && Y == 0;

    // Expression-bodied method returning tuple
    public (double x, double y) AsAngle() => (
        X / Distance(),
        Y / Distance()
    );

    // Explicit conversion using expression body
    public static explicit operator Point(string coords)
    {
        var parts = coords.Split(',');
        return new Point(double.Parse(parts[0]), double.Parse(parts[1]));
    }

    // ToString as expression body
    public override string ToString() => $"({X}, {Y})";
}

// Record type (C# 9.0+) uses expression bodies by default
public record Rectangle(double Width, double Height)
{
    public double Area => Width * Height;
    public double Perimeter => 2 * (Width + Height);
    public bool IsSquare => Width == Height;
}

Read full guide →

Extension methods

C# 3.0 NETFx 3.5

Extension methods let you add discoverable helpers to existing types, which is why they show up everywhere from LINQ to application-specific fluent APIs.

Adding a fluent helper to a string

Extension methods let you add reusable helpers without changing the original type.

public static class StringExtensions
{
    public static bool IsNullOrEmptyTrimmed(this string value) =>
        string.IsNullOrWhiteSpace(value?.Trim());
}

Read full guide →

File-local types

C# 11.0 .NET 7.0

File-local types (`file class`, `file record`, and similar declarations) keep implementation-only types visible to one source file.

Declaring helper types with the file modifier

File-local types keep implementation details in the same file while preventing those types from leaking into project-wide namespaces.

using System;

public sealed class UserCache
{
    public string CreateCacheKey(int userId, DateTime snapshotUtc)
    {
        var key = new UserCacheKey(userId, snapshotUtc);
        return key.ToString();
    }
}

file readonly record struct UserCacheKey(int UserId, DateTime SnapshotUtc)
{
    public override string ToString() => $"user:{UserId}:snapshot:{SnapshotUtc:yyyyMMddHHmmss}";
}

public static class Program
{
    public static void Main()
    {
        var cache = new UserCache();
        string key = cache.CreateCacheKey(42, new DateTime(2026, 7, 10, 9, 30, 0, DateTimeKind.Utc));
        Console.WriteLine(key);
    }
}

From nested private classes to file-local collaborators

Move deeply nested helper types out of containing classes while keeping them hidden from other files through the file modifier.

Before

using System;
using System.Collections.Generic;
using System.Globalization;

public sealed class ReportFormatter
{
    public string Format(IReadOnlyList<decimal> values)
    {
        var rowFormatter = new RowFormatter();
        return rowFormatter.Format(values);
    }

    private sealed class RowFormatter
    {
        public string Format(IReadOnlyList<decimal> values)
        {
            return string.Join(", ", values).Replace(",", CultureInfo.InvariantCulture.NumberFormat.NumberGroupSeparator);
        }
    }
}

public static class Program
{
    public static void Main()
    {
        var formatter = new ReportFormatter();
        Console.WriteLine(formatter.Format(new[] { 12.5m, 48.3m, 105.7m }));
    }
}

After

using System;
using System.Collections.Generic;
using System.Globalization;

public sealed class ReportFormatter
{
    public string Format(IReadOnlyList<decimal> values)
    {
        var rowFormatter = new RowFormatter();
        return rowFormatter.Format(values);
    }
}

file sealed class RowFormatter
{
    public string Format(IReadOnlyList<decimal> values)
    {
        var formatted = new List<string>(values.Count);
        foreach (decimal value in values)
        {
            formatted.Add(value.ToString("N1", CultureInfo.InvariantCulture));
        }

        return string.Join(" | ", formatted);
    }
}

public static class Program
{
    public static void Main()
    {
        var formatter = new ReportFormatter();
        Console.WriteLine(formatter.Format(new[] { 12.5m, 48.3m, 105.7m }));
    }
}

Read full guide →

File-Scoped Namespaces

C# 10.0 .NET 6.0

File-scoped namespaces declare a namespace at the file level with a simple semicolon syntax, eliminating unnecessary indentation and making code cleaner and easier to read.

Using file-scoped namespaces

File-scoped namespaces eliminate indentation and reduce nesting, applying the namespace to the entire file with a single declaration.

// UserService.cs - using file-scoped namespace (C# 10.0)
namespace MyApp.Services;

public class UserService
{
    public void PrintUser(string name)
    {
        Console.WriteLine($"User: {name}");
    }
}

// Logger class in the same file-scoped namespace
public class Logger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

Comparison with traditional namespaces

See how file-scoped namespaces simplify code structure compared to traditional block-scoped namespaces.

// TRADITIONAL BLOCK-SCOPED NAMESPACE (pre-C# 10.0)
/*
namespace MyApp.Services
{
    public class UserService
    {
        public void PrintUser(string name)
        {
            Console.WriteLine($"User: {name}");
        }
    }
}
*/

// FILE-SCOPED NAMESPACE (C# 10.0+)
namespace MyApp.Services;

public class UserService
{
    public void PrintUser(string name)
    {
        Console.WriteLine($"User: {name}");
    }
}

// Benefits:
// - Cleaner indentation
// - Simpler visual structure
// - Less nesting required
// - Modern, recommended approach

Read full guide →

Func<T> and Action<T>

C# 3.0 NETFx 3.5

Use built-in generic delegate types to pass behavior without defining one-off delegate types.

Use Func<T> to pass calculations

Func delegates are ideal when behavior should be injected and a value returned.

using System;

class Program
{
    static int Apply(int left, int right, Func<int, int, int> operation)
    {
        return operation(left, right);
    }

    static void Main()
    {
        int sum = Apply(10, 5, (x, y) => x + y);
        int product = Apply(10, 5, (x, y) => x * y);

        Console.WriteLine(sum);      // 15
        Console.WriteLine(product);  // 50
    }
}

Use Action<T> for side-effect steps

Action delegates make it easy to pass work that does not return a value.

using System;

class Program
{
    static void ForEachName(string[] names, Action<string> handleName)
    {
        foreach (string name in names)
        {
            handleName(name);
        }
    }

    static void Main()
    {
        string[] names = { "Ana", "Lee", "Sam" };
        ForEachName(names, name => Console.WriteLine("Hello, " + name));
    }
}

Read full guide →

Global Using Directives

C# 10.0 .NET 6.0

Global using directives allow you to specify imports that automatically apply to all files in your project, reducing repetitive using statements and improving code organization at scale.

Using global directives

Global using directives apply to all files in your project, eliminating the need to repeat common imports in every file.

// Usings.cs (or Program.cs) - define global using statements
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Text.Json;
global using System.Threading.Tasks;

// Now any other file in the project can use these namespaces
// without repeating the using statements

// Example.cs (no using statements needed)
namespace MyApp;

public class UserService
{
    public async Task<List<User>> GetUsersAsync()
    {
        // Can use List<T>, Task, etc. without local using statements
        return new List<User>();
    }
}

public record User(int Id, string Name);

Global static using

Use global static using to import static members from a type once for the entire project.

// Usings.cs - define global using statements including static usings
global using System;
global using static System.Console;
global using static System.Math;

// Now WriteLine and static Math members are available everywhere

// Calculator.cs
namespace MyApp;

public class Calculator
{
    public double CalculateHypotenuse(double a, double b)
    {
        // Can use WriteLine and Sqrt without System or Math prefixes
        double result = Sqrt(Pow(a, 2) + Pow(b, 2));
        WriteLine($"Hypotenuse: {result}");
        return result;
    }
}

Read full guide →

Init accessors

C# 9.0 .NET 5.0

Init accessors let you keep object initialization simple while still protecting properties from later mutation.

Creating an object that can only be set during initialization

Init-only setters keep object construction flexible while preventing mutation after the initializer finishes.

public sealed class Customer
{
    public string Name { get; init; } = string.Empty;
    public int LoyaltyPoints { get; init; }
}

Read full guide →

Lambda expressions

C# 3.0 NETFx 3.5

Lambda expressions make delegate-based code shorter and easier to scan, especially when you pass behavior into LINQ, events, and async APIs.

Replacing anonymous methods with a lambda

Lambda expressions let you write compact delegates inline without losing type safety.

Func<int, int> doubleIt = value => value * 2;

Read full guide →

LINQ

C# 3.0 NETFx 3.5

LINQ gives C# a consistent way to filter, transform, group, and sort data with either query syntax or fluent method syntax.

Filtering and projecting data

LINQ turns sequence processing into a readable pipeline instead of nested loops and temp variables.

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);

Read full guide →

List patterns

C# 11.0 .NET 7.0

Match specific positions and elements in arrays or lists using patterns, reducing complex index-based conditionals to readable, declarative expressions.

Match array elements by position and value

List patterns allow you to match specific positions and values in an array or list, enabling powerful pattern-based control flow.

Before

int[] numbers = [1, 2, 3];
if (numbers.Length >= 2 && numbers[0] == 1 && numbers[1] == 2)
{
    Console.WriteLine("Starts with 1, 2");
}
else if (numbers.Length == 3 && numbers[0] == 1 && numbers[2] == 3)
{
    Console.WriteLine("Pattern: 1, any, 3");
}

After

int[] numbers = [1, 2, 3];
string result = numbers switch
{
    [1, 2, ..] => "Starts with 1, 2",
    [1, _, 3] => "Pattern: 1, any, 3",
    _ => "No match"
};
Console.WriteLine(result);

Slice patterns for variable-length lists

List patterns with slice patterns (`..`) allow matching the first, last, or middle elements while ignoring or capturing the rest.

string Summarize(int[] numbers) => numbers switch
{
    [var first, .. var rest] => $"First: {first}, Rest count: {rest.Length}",
    [var only] => $"Single element: {only}",
    [] => "Empty list"
};

Read full guide →

Local functions

C# 7.0 NETFx 4.7

Local functions are methods defined inside other methods or properties, letting you encapsulate helper logic at the point of use. They reduce namespace clutter, improve readability by keeping related logic together, and naturally capture variables from their enclosing scope.

Basic local functions

Define helper functions inside methods to encapsulate logic and reduce pollution of class scope.

// Before: Separate private methods (clutters class interface)
public class OrderProcessorOld
{
    public void ProcessOrder(Order order)
    {
        ValidateOrderInternal(order);
        CalculateTotal(order);
        ShipOrder(order);
    }

    private void ValidateOrderInternal(Order order)
    {
        if (order == null) throw new ArgumentNullException(nameof(order));
        if (order.Items.Count == 0) throw new ArgumentException("Order is empty");
        if (order.Items.Any(item => item.Quantity <= 0))
            throw new ArgumentException("Invalid quantity");
    }

    private void CalculateTotal(Order order)
    {
        order.Total = order.Items.Sum(item => item.Price * item.Quantity);
    }
}

// After: Local functions (encapsulated at point of use)
public class OrderProcessorNew
{
    public void ProcessOrder(Order order)
    {
        ValidateOrder(order);
        CalculateTotal(order);
        ShipOrder(order);

        // Local validation function - visible only to ProcessOrder
        void ValidateOrder(Order o)
        {
            if (o == null) throw new ArgumentNullException(nameof(o));
            if (o.Items.Count == 0) throw new ArgumentException("Order is empty");
            if (o.Items.Any(item => item.Quantity <= 0))
                throw new ArgumentException("Invalid quantity");
        }

        // Local calculation function
        void CalculateTotal(Order o)
        {
            o.Total = o.Items.Sum(item => item.Price * item.Quantity);
        }
    }

    private void ShipOrder(Order order) { /* ... */ }
}

public class Order
{
    public List<OrderItem> Items { get; set; } = new();
    public decimal Total { get; set; }
}

public class OrderItem
{
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}

Local functions with recursion and closures

Use local functions for recursive algorithms and to capture variables from the enclosing scope.

public class LocalFunctionsAdvanced
{
    // Recursive local function
    public long Factorial(int n)
    {
        if (n < 0) throw new ArgumentException("n must be non-negative");

        return FactorialLocal(n);

        long FactorialLocal(int x) => x <= 1 ? 1 : x * FactorialLocal(x - 1);
    }

    // Fibonacci with memoization using local function
    public int Fibonacci(int n)
    {
        var cache = new Dictionary<int, int>();

        return FibLocal(n);

        int FibLocal(int x)
        {
            if (x <= 1) return x;
            if (cache.TryGetValue(x, out int cached)) return cached;

            int result = FibLocal(x - 1) + FibLocal(x - 2);
            cache[x] = result;
            return result;
        }
    }

    // Local functions capturing outer variables (closure)
    public Func<int, int> CreateMultiplier(int factor)
    {
        // Local function captures 'factor' from enclosing scope
        int Multiply(int x) => x * factor;

        // Return as delegate
        return Multiply;
    }

    // Multiple local functions with interdependencies
    public void GenerateReport(List<int> data)
    {
        var sum = 0;
        var max = int.MinValue;

        ProcessData();
        PrintSummary();

        void ProcessData()
        {
            foreach (var value in data)
            {
                sum += value;
                if (value > max) max = value;
            }
        }

        void PrintSummary()
        {
            double average = data.Count > 0 ? (double)sum / data.Count : 0;
            Console.WriteLine($"Sum: {sum}, Average: {average:F2}, Max: {max}");
        }
    }

    // TreeNode for recursive example
    public class TreeNode
    {
        public int Value { get; set; }
        public TreeNode Left { get; set; }
        public TreeNode Right { get; set; }
    }

    // Binary tree traversal using local recursion
    public List<int> InOrderTraversal(TreeNode root)
    {
        var result = new List<int>();

        Traverse(root);
        return result;

        void Traverse(TreeNode node)
        {
            if (node == null) return;

            Traverse(node.Left);
            result.Add(node.Value);
            Traverse(node.Right);
        }
    }
}

Local async functions and iterators

Combine local functions with async/await and yield to create powerful iterative and asynchronous patterns.

using System;
using System.Collections.Generic;

public class LocalFunctionsAsyncAndIterators
{
    // Local async function
    public async System.Threading.Tasks.Task<string> FetchDataAsync(string url)
    {
        var client = new System.Net.Http.HttpClient();
        var data = await FetchInternal();
        return data;

        async System.Threading.Tasks.Task<string> FetchInternal()
        {
            var response = await client.GetAsync(url);
            return await response.Content.ReadAsStringAsync();
        }
    }

    // Local iterator function (generator)
    public IEnumerable<int> GenerateSequence(int count)
    {
        return GetNumbers();

        IEnumerable<int> GetNumbers()
        {
            for (int i = 0; i < count; i++)
            {
                yield return i * 2;
            }
        }
    }

    // Local async iterator (C# 8.0+)
    public async System.Collections.Generic.IAsyncEnumerable<int> FetchNumbersAsync(int count)
    {
        await foreach (var num in GetNumbersAsync())
        {
            yield return num;
        }

        async IAsyncEnumerable<int> GetNumbersAsync()
        {
            for (int i = 0; i < count; i++)
            {
                await System.Threading.Tasks.Task.Delay(100); // Simulate async work
                yield return i;
            }
        }
    }

    // Local function with lazy evaluation
    public IEnumerable<T> FilterAndTransform<T>(IEnumerable<T> source, Func<T, bool> predicate, Func<T, T> transform)
    {
        return ExecuteTransform();

        IEnumerable<T> ExecuteTransform()
        {
            foreach (var item in source)
            {
                if (predicate(item))
                {
                    yield return transform(item);
                }
            }
        }
    }

    // Combining multiple local functions with async
    public async System.Threading.Tasks.Task<(int success, int failed)> ProcessItemsAsync(string[] items)
    {
        int successCount = 0;
        int failCount = 0;

        foreach (var item in items)
        {
            if (await TryProcess(item))
                successCount++;
            else
                failCount++;
        }

        return (successCount, failCount);

        async System.Threading.Tasks.Task<bool> TryProcess(string item)
        {
            try
            {
                await ProcessItemAsync(item);
                return true;
            }
            catch
            {
                return false;
            }
        }

        async System.Threading.Tasks.Task ProcessItemAsync(string item)
        {
            await System.Threading.Tasks.Task.Delay(50); // Simulate work
            Console.WriteLine($"Processed: {item}");
        }
    }

    // Local async function with resource management
    public async System.Threading.Tasks.Task<int> CountLinesAsync(string filePath)
    {
        return await CountLines();

        async System.Threading.Tasks.Task<int> CountLines()
        {
            int count = 0;
            using (var reader = new System.IO.StreamReader(filePath))
            {
                string line;
                while ((line = await reader.ReadLineAsync()) != null)
                {
                    if (!string.IsNullOrWhiteSpace(line))
                        count++;
                }
            }
            return count;
        }
    }
}

Read full guide →

Named and optional parameters

C# 4.0 NETFx 4.5

Named arguments and default values make APIs easier to read without multiplying overloads.

Make multi-flag calls readable

Named arguments remove guesswork when a method has several parameters of the same shape or a few optional switches.

Before

using System;

public sealed class NotificationService
{
    public void Send(string recipient, string subject, string body, bool highPriority, bool saveCopy)
    {
        Console.WriteLine("To: " + recipient);
        Console.WriteLine("Subject: " + subject);
        Console.WriteLine(
            string.Format(
                "Priority: {0} | Save copy: {1}",
                highPriority ? "high" : "normal",
                saveCopy));
        Console.WriteLine(body);
    }
}

public static class Program
{
    public static void Main()
    {
        var service = new NotificationService();
        service.Send(
            "team@contoso.com",
            "Deployment complete",
            "The new release is live.",
            true,
            false);
    }
}

After

using System;

public sealed class NotificationService
{
    public void Send(
        string recipient,
        string subject,
        string body,
        bool highPriority = false,
        bool saveCopy = true)
    {
        Console.WriteLine("To: " + recipient);
        Console.WriteLine("Subject: " + subject);
        Console.WriteLine(
            string.Format(
                "Priority: {0} | Save copy: {1}",
                highPriority ? "high" : "normal",
                saveCopy));
        Console.WriteLine(body);
    }
}

public static class Program
{
    public static void Main()
    {
        var service = new NotificationService();
        service.Send(
            recipient: "team@contoso.com",
            subject: "Deployment complete",
            body: "The new release is live.",
            highPriority: true,
            saveCopy: false);
    }
}

Trim overloads with optional defaults

Optional parameters let one method cover the common path while still allowing targeted customization.

using System;

public static class InvoiceFormatter
{
    public static string FormatInvoice(
        string customerName,
        decimal amount,
        string currency = "USD",
        bool includeDueDate = false)
    {
        var message = string.Format(
            "Invoice for {0}: {1:0.00} {2}",
            customerName,
            amount,
            currency);

        return includeDueDate
            ? message + " (due in 14 days)"
            : message;
    }
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine(InvoiceFormatter.FormatInvoice("A. Datum", 1200m));
        Console.WriteLine(
            InvoiceFormatter.FormatInvoice(
                "A. Datum",
                1200m,
                currency: "EUR",
                includeDueDate: true));
    }
}

Read full guide →

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.

Capture failed conditions in guard helpers

Pair CallerArgumentExpression with nameof to produce precise diagnostics without hard-coded parameter strings.

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}");
        }
    }
}

Build null-check helpers with rich error messages

Capture the original call-site expression so thrown exceptions tell you exactly what argument failed validation.

using System;
using System.Runtime.CompilerServices;

public static class Ensure
{
    public static T NotNull<T>(
        T? value,
        [CallerArgumentExpression("value")] string? expression = null,
        [CallerMemberName] string? caller = null)
        where T : class
    {
        if (value is null)
        {
            throw new ArgumentNullException(
                nameof(value),
                $"'{expression}' was null when called from {caller}.");
        }

        return value;
    }
}

public static class Program
{
    public static void Main()
    {
        string? userName = null;

        try
        {
            _ = Ensure.NotNull(userName);
        }
        catch (ArgumentNullException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

Read full guide →

Null coalescing & assignment operators

C# 8.0 NETCore 3.0

The null coalescing operator (`??`) and null coalescing assignment operator (`??=`) provide concise ways to handle null values, letting you specify fallback values or conditionally assign without verbose null checks.

Null coalescing operator (??)

Provide a default value when the left operand is null.

// Before: Verbose null checks
public class UserProfileOld
{
    public string? GetDisplayName(User? user)
    {
        if (user == null)
            return "Unknown";
        return user.Name;
    }
}

// After: Concise with null coalescing
public class UserProfileNew
{
    public string GetDisplayName(User? user)
    {
        return user?.Name ?? "Unknown";
    }
}

public class User
{
    public string Name { get; set; }
}

Null coalescing assignment (??=)

Assign a value only if the variable is currently null.

public class ConfigurationLoader
{
    private Dictionary<string, string>? _cache;

    // Before: Explicit null assignment
    public Dictionary<string, string> GetCacheOld()
    {
        if (_cache == null)
        {
            _cache = new Dictionary<string, string>();
        }
        return _cache;
    }

    // After: Concise with ??= operator
    public Dictionary<string, string> GetCacheNew()
    {
        _cache ??= new Dictionary<string, string>();
        return _cache;
    }

    // Common pattern: lazy initialization in properties
    private List<string>? _errors;

    public List<string> Errors
    {
        get => _errors ??= new List<string>();
    }
}

Chaining multiple null checks

Use the null coalescing operator to check multiple fallback values in sequence.

public class DataExtractor
{
    public class UserPreferences
    {
        public string? Theme { get; set; }
        public string? Locale { get; set; }
        public string? Timezone { get; set; }
    }

    // Chain multiple null checks with ?? operator
    public string GetTheme(UserPreferences? prefs, string systemDefault, string hardcodedDefault)
    {
        // Try user preferences → system default → hardcoded fallback
        return prefs?.Theme ?? systemDefault ?? hardcodedDefault;
    }

    // Real-world: API response handling
    public class ApiResponse
    {
        public string? Message { get; set; }
        public string? ErrorCode { get; set; }
    }

    public string GetUserMessage(ApiResponse? response, string requestId)
    {
        // If response is null, or message is null, use error code or request ID
        return response?.Message ?? response?.ErrorCode ?? $"Request {requestId} failed";
    }

    // Combining with null-conditional operator
    public int? GetConfigValue(Dictionary<string, int?>? configs, string key, int? defaultValue)
    {
        return configs?[key] ?? defaultValue ?? 0;
    }
}

Read full guide →

Nullable reference types

C# 8.0 NETCore 3.0

Nullable reference types make nullability part of the type system, which helps the compiler catch bugs before they reach production.

Marking a reference as optional

Nullable annotations force you to model absence explicitly instead of assuming every reference is populated.

string? FindDisplayName() => null;

string? displayName = FindDisplayName();

if (displayName is null)
{
    return;
}

Console.WriteLine(displayName.Length);

Read full guide →

Object and collection initializers

C# 3.0 NETFx 3.5

Object and collection initializers let you populate properties and collection elements inline at construction time. Instead of creating an object and then setting properties separately, you do it all in one readable expression.

Building objects with initializer syntax

Use object initializers to construct and populate an object in one readable expression.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var person = new Person { Name = "Alice", Age = 30 };

Initializing collections inline

Collection initializers let you populate lists, dictionaries, and other collections with values at construction time.

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var names = new[] { "Alice", "Bob", "Charlie" };

Nesting objects and collections

Combine object and collection initializers to build complex structures inline, keeping code readable.

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

var person = new Person
{
    Name = "Alice",
    Address = new Address { Street = "123 Main St", City = "Seattle" }
};

Read full guide →

out, ref, and in Parameters

C# 7.2 NETCore 2.1

Control argument passing by reference to return extra values, mutate callers, or avoid copies for readonly inputs.

Use out for parse-style APIs

out parameters return extra values while signaling success or failure.

using System;

if (int.TryParse("123", out int value))
{
    Console.WriteLine(value + 1);
}
else
{
    Console.WriteLine("Invalid number");
}

Use ref when caller state should be updated

ref parameters let methods modify caller variables directly.

using System;

static void Increment(ref int number)
{
    number++;
}

int count = 10;
Increment(ref count);
Console.WriteLine(count); // 11

Use in for readonly pass-by-reference

in passes large value types by reference while preventing modification.

using System;

public readonly struct Vector3
{
    public Vector3(double x, double y, double z)
    {
        X = x;
        Y = y;
        Z = z;
    }

    public double X { get; }
    public double Y { get; }
    public double Z { get; }
}

public static class Program
{
    private static double Length(in Vector3 value)
    {
        return Math.Sqrt((value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z));
    }

    public static void Main()
    {
        var vector = new Vector3(3, 4, 12);
        Console.WriteLine(Length(in vector));
    }
}

Read full guide →

Pattern matching

C# 9.0 .NET 5.0

Pattern matching gives C# a precise branching model for types, values, and shapes, which makes complex decisions easier to read.

Branching with relational and logical patterns

Modern pattern matching lets you describe ranges and combinations directly in the branch expression.

string DescribeTemperature(int celsius) => celsius switch
{
    < 0 => "freezing",
    >= 0 and <= 10 => "cold",
    > 10 and < 25 => "mild",
    _ => "warm"
};

Read full guide →

Primary constructors

C# 12.0 .NET 8.0

Declare constructor parameters directly in the class declaration, eliminating the need for explicit constructor bodies and field assignments.

Declare and initialize properties inline

Primary constructors let you declare constructor parameters directly in the class declaration, automatically creating and initializing backing fields.

Before

public class Person
{
    private string name;
    private int age;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public string GetName() => name;
}

After

public class Person(string name, int age)
{
    private string Name => name;
    private int Age => age;

    public string GetName() => Name;
}

Record-like initialization without records

Primary constructors bring record-style initialization convenience to regular classes, reducing boilerplate while maintaining explicit class semantics.

var person = new Person("Alice", 30);
Console.WriteLine(person);

public class Person(string name, int age)
{
    public string Name { get; } = name;
    public int Age { get; } = age;

    public override string ToString() => $"{Name} ({Age} years old)";
}

Read full guide →

Range and index operators

C# 8.0 NETCore 3.0

Range (..) and index (^) operators provide concise syntax for accessing array elements and extracting subsequences. The index operator lets you count from the end; the range operator extracts contiguous slices without manual offset calculations.

Index operator (^)

Access array elements from the end using the ^ operator.

// Before: Manual offset arithmetic
public class ArrayManipulationOld
{
    public int GetLastElement(int[] numbers)
    {
        return numbers[numbers.Length - 1];
    }

    public int[] GetLastThreeElements(int[] numbers)
    {
        int start = numbers.Length - 3;
        int[] result = new int[3];
        System.Array.Copy(numbers, start, result, 0, 3);
        return result;
    }

    public string ExtractDomain(string email)
    {
        int atIndex = email.IndexOf('@');
        return email.Substring(atIndex + 1);
    }
}

// After: Index and range operators
public class ArrayManipulationNew
{
    public int GetLastElement(int[] numbers)
    {
        return numbers[^1];
    }

    public int[] GetLastThreeElements(int[] numbers)
    {
        return numbers[^3..];
    }

    public string ExtractDomain(string email)
    {
        int atIndex = email.IndexOf('@');
        return email[(atIndex + 1)..];
    }
}

Range operator (..)

Extract contiguous subsequences from arrays and strings.

public class RangeOperatorExamples
{
    public void ArrayRanges()
    {
        var numbers = new[] { 10, 20, 30, 40, 50, 60, 70, 80 };

        // Range from start
        int[] firstThree = numbers[..3];           // [10, 20, 30]

        // Range to end
        int[] lastThree = numbers[5..];            // [60, 70, 80]

        // Range in middle
        int[] middle = numbers[2..5];              // [30, 40, 50]

        // Full range (creates a copy)
        int[] copy = numbers[..];                  // all elements
    }

    public void StringRanges()
    {
        string path = "C:\\Users\\Documents\\file.txt";

        // Extract filename
        int lastSlash = path.LastIndexOf('\\');
        string fileName = path[(lastSlash + 1)..]; // "file.txt"

        // Extract extension
        int lastDot = path.LastIndexOf('.');
        string extension = path[lastDot..];        // ".txt"

        // Extract all but extension
        string nameOnly = path[..lastDot];         // "C:\\Users\\Documents\\file"
    }

    public void SpanRanges()
    {
        Span<byte> buffer = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };

        // Extract header (first 2 bytes)
        Span<byte> header = buffer[..2];

        // Extract payload (skip first 2 bytes)
        Span<byte> payload = buffer[2..];

        // Extract specific frame (zero-copy operation)
        Span<byte> frame = buffer[1..4];
    }
}

Combining range and index operators

Use range and index operators together for powerful sequence manipulation.

public class CombinedRangeAndIndex
{
    public void PaginationExample()
    {
        var allItems = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
        int pageSize = 3;
        int pageNumber = 1; // 0-indexed

        // Extract one page using range
        var page = allItems[(pageNumber * pageSize)..((pageNumber + 1) * pageSize)];
        // page = [4, 5, 6]
    }

    public void LogRotationExample()
    {
        var logLines = new[]
        {
            "[INFO] App started",
            "[DEBUG] Loading config",
            "[INFO] Config loaded",
            "[WARN] Cache miss",
            "[ERROR] Connection failed",
            "[INFO] Retry attempt 1"
        };

        // Get all lines except the first and last (trim headers and summary)
        var mainContent = logLines[1..^1];

        // Get last 3 lines (recent activity)
        var recent = logLines[^3..];

        // Get errors and warnings (assuming they're at specific indices)
        var criticalLines = logLines[4..5]; // Just the error
    }

    public void DataValidationExample()
    {
        string csvLine = "123,John,Doe,john@example.com,2025-01-15";
        var fields = csvLine.Split(',');

        // Extract required fields using index from end
        string email = fields[^2];            // "john@example.com"
        string dateStr = fields[^1];          // "2025-01-15"

        // Extract name parts (first two fields after ID)
        string fullName = string.Join(" ", fields[1..3]); // "John Doe"

        // Validate: must have at least 5 fields
        bool valid = fields.Length >= 5;
    }

    public void SlidingWindowExample()
    {
        var values = new[] { 10, 20, 30, 40, 50, 60, 70, 80 };
        int windowSize = 3;

        // Calculate moving average
        double[] movingAverages = new double[values.Length - windowSize + 1];

        for (int i = 0; i < movingAverages.Length; i++)
        {
            var window = values[i..(i + windowSize)];
            movingAverages[i] = window.Average();
        }
        // movingAverages[0] = 20 (avg of 10, 20, 30)
        // movingAverages[1] = 30 (avg of 20, 30, 40)
    }
}

Read full guide →

Raw String Literals

C# 11.0 .NET 7.0

Raw string literals are delimited by three or more quotes and eliminate escape-sequence processing, making it easy to work with JSON, SQL, and multiline text.

Basic raw string literals

Raw strings eliminate escape sequences, allowing you to write JSON, SQL, and multiline text naturally without escaping quotes or backslashes.

// JSON without raw strings - requires escaping quotes
string jsonOld = "{\"name\": \"Alice\", \"age\": 30}";

// JSON with raw strings - clean and readable
string jsonNew = """
{
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com"
}
""";

// SQL query with raw strings
string sqlQuery = """
SELECT u.Id, u.Name, u.Email
FROM Users u
WHERE u.Status = 'Active'
ORDER BY u.Name
""";

// Regex pattern with raw strings
string patternOld = "^\\w+@[\\w\\.]+\\.\\w+$";
string patternNew = """^\\w+@[\w\.]+\.\w+$""";

Console.WriteLine(jsonNew);
Console.WriteLine(sqlQuery);

Raw strings with interpolation

Combine raw string literals with interpolation for clean, readable embedded queries and templates with variable substitution.

// Raw string with interpolation
string name = "Alice";
int age = 30;
string email = "alice@example.com";

// Combine raw strings with interpolation ($)
string jsonResponse = $$"""
{
    "name": "{{name}}",
    "age": {{age}},
    "email": "{{email}}"
}
""";

// Dynamic SQL query with safe parameter substitution
string userId = "42";
string sqlQuery = $$"""
SELECT * FROM Orders
WHERE UserId = {{userId}}
AND OrderDate >= DATEADD(day, -30, GETDATE())
""";

Console.WriteLine(jsonResponse);
Console.WriteLine(sqlQuery);

Read full guide →

Records

C# 9.0 .NET 5.0

Records are a concise way to model value-oriented data with built-in equality, deconstruction, and non-destructive mutation.

Modeling immutable data with a record

Records are concise reference types with value-based equality and built-in support for non-destructive updates.

var today = new WeatherReading("Seattle", 9.5m);
var warmer = today with { TemperatureC = 12.0m };

public record WeatherReading(string City, decimal TemperatureC);

Read full guide →

ref returns

C# 7.0 NETFx 4.7

Ref returns hand back a reference to existing storage so callers can work in place instead of through a copy.

Return the storage slot instead of a copy

A ref return lets the caller update the original array element without searching twice or copying values around.

using System;

public static class Program
{
    public static void Main()
    {
        int[] scores = new[] { 78, 84, 91 };
        ref int bestScore = ref FindLargest(scores);
        bestScore += 5;

        Console.WriteLine(string.Join(", ", scores));
    }

    private static ref int FindLargest(int[] values)
    {
        int largestIndex = 0;

        for (int index = 1; index < values.Length; index++)
        {
            if (values[index] > values[largestIndex])
            {
                largestIndex = index;
            }
        }

        return ref values[largestIndex];
    }
}

Expose an in-place update path from a type

Types that own arrays or buffers can offer targeted mutation without exposing the whole storage implementation.

using System;
using System.Linq;

public sealed class ScoreBoard
{
    private readonly int[] _scores = new int[4];

    public ref int this[int index] => ref _scores[index];

    public override string ToString() => string.Join(", ", _scores.Select(score => score.ToString()));
}

public static class Program
{
    public static void Main()
    {
        ScoreBoard board = new ScoreBoard();
        board[0] = 10;

        ref int lateGameBonusSlot = ref board[2];
        lateGameBonusSlot = 42;

        Console.WriteLine(board);
    }
}

Read full guide →

ref structs

C# 7.2 NETCore 2.1

`ref struct` types stay on the stack, which makes span-heavy APIs safe and allocation-conscious.

Build a small parser over a span

A `ref struct` can safely hold `ReadOnlySpan<T>` state while it walks in-place data without allocating substrings.

using System;

public ref struct CsvFieldReader
{
    private ReadOnlySpan<char> _remaining;

    public CsvFieldReader(ReadOnlySpan<char> line)
    {
        _remaining = line;
    }

    public bool TryRead(out ReadOnlySpan<char> field)
    {
        if (_remaining.IsEmpty)
        {
            field = default;
            return false;
        }

        var commaIndex = _remaining.IndexOf(',');
        if (commaIndex < 0)
        {
            field = _remaining;
            _remaining = default;
            return true;
        }

        field = _remaining.Slice(0, commaIndex);
        _remaining = _remaining.Slice(commaIndex + 1);
        return true;
    }
}

public static class Program
{
    public static void Main()
    {
        CsvFieldReader reader = new CsvFieldReader("Ada Lovelace,Mathematician,London".AsSpan());

        ReadOnlySpan<char> field;
        while (reader.TryRead(out field))
        {
            Console.WriteLine(field.ToString());
        }
    }
}

Wrap stackalloc memory in a focused helper

`ref struct` helpers are a natural way to keep stack-only buffers together with the operations that use them.

using System;

public ref struct AsciiScratchBuffer
{
    private Span<byte> _buffer;

    public AsciiScratchBuffer(Span<byte> buffer)
    {
        _buffer = buffer;
    }

    public int WriteUppercase(ReadOnlySpan<char> text)
    {
        var written = 0;

        foreach (var character in text)
        {
            _buffer[written++] = (byte)char.ToUpperInvariant(character);
        }

        return written;
    }
}

public static class Program
{
    public static void Main()
    {
        Span<byte> bytes = stackalloc byte[32];
        AsciiScratchBuffer scratch = new AsciiScratchBuffer(bytes);
        int length = scratch.WriteUppercase("ok-42".AsSpan());

        for (int index = 0; index < length; index++)
        {
            Console.Write((char)bytes[index]);
        }
    }
}

Read full guide →

Required Members

C# 11.0 .NET 7.0

Required members enforce that specific properties must be set during object initialization, preventing incomplete objects and catching errors at compile time.

Marking properties as required

The required keyword ensures that properties must be initialized when creating an instance, with compile-time enforcement.

using System;

// Immutable record with required init-only properties
public record User
{
    public required int Id { get; init; }
    public required string Email { get; init; }
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public string? MiddleName { get; init; }
}

public static class Program
{
    public static void Main()
    {
        // This compiles - all required properties are initialized
        var user = new User
        {
            Id = 1,
            Email = "alice@example.com",
            FirstName = "Alice",
            LastName = "Smith"
        };

        // This does NOT compile
        // var incomplete = new User
        // {
        //     Id = 2,
        //     Email = "bob@example.com"
        //     // Error: Missing required member 'FirstName' and 'LastName'
        // };

        Console.WriteLine($"User: {user.FirstName} {user.LastName} ({user.Email})");
    }
}

Required with init-only properties

Combine required with init-only properties for immutable objects with compile-time initialization guarantees.

using System;

// Product class with required members
public class Product
{
    public required string Name { get; set; }
    public required decimal Price { get; set; }
    public string? Description { get; set; }
    public int? StockCount { get; set; }
}

public static class Program
{
    public static void Main()
    {
        // This compiles - all required properties are set
        var product1 = new Product
        {
            Name = "Laptop",
            Price = 999.99m,
            Description = "High-performance laptop"
        };

        // This does NOT compile - missing required property 'Price'
        // var product2 = new Product
        // {
        //     Name = "Tablet"
        //     // Error: Missing required member 'Price'
        // };

        Console.WriteLine($"Product: {product1.Name}, Price: {product1.Price}");
    }
}

Read full guide →

Span<T> and ReadOnlySpan<T>

C# 7.2 NETCore 2.1

Work with contiguous memory efficiently using safe slices that avoid many temporary allocations.

Slice arrays without allocation

Span slices create lightweight views over existing memory instead of new arrays.

using System;

int[] values = { 5, 10, 15, 20, 25, 30 };
Span<int> window = values.AsSpan(1, 3);

for (int i = 0; i < window.Length; i++)
{
    window[i] *= 2;
}

Console.WriteLine(string.Join(", ", values)); // 5, 20, 30, 40, 25, 30

Use ReadOnlySpan<char> over strings

ReadOnlySpan<char> lets you process substrings without allocating new string instances.

using System;

string csv = "alpha,beta,gamma";
ReadOnlySpan<char> text = csv.AsSpan();
int separatorIndex = text.IndexOf(',');

ReadOnlySpan<char> firstToken = text[..separatorIndex];
ReadOnlySpan<char> remainder = text[(separatorIndex + 1)..];

Console.WriteLine(firstToken.ToString()); // alpha
Console.WriteLine(remainder.ToString());  // beta,gamma

Read full guide →

Static Abstract Members in Interfaces

C# 11.0 .NET 7.0

Define compile-time contracts for static members so generic algorithms can call them safely.

Constrain generic parsing with static abstract members

Call static members through generic constraints for compile-time-safe abstractions.

using System;

public interface ISpanParsableValue<TSelf> where TSelf : ISpanParsableValue<TSelf>
{
    static abstract TSelf Parse(ReadOnlySpan<char> value);
}

public readonly record struct ProductCode(string Value) : ISpanParsableValue<ProductCode>
{
    public static ProductCode Parse(ReadOnlySpan<char> value) => new(value.ToString().Trim().ToUpperInvariant());
}

public static class Program
{
    private static T ParseValue<T>(ReadOnlySpan<char> input) where T : ISpanParsableValue<T>
    {
        return T.Parse(input);
    }

    public static void Main()
    {
        var code = ParseValue<ProductCode>(" abc-42 ");
        Console.WriteLine(code.Value); // ABC-42
    }
}

Model reusable numeric operations

Static abstract operators enable type-safe generic math patterns.

using System;

public interface IAddable<TSelf> where TSelf : IAddable<TSelf>
{
    static abstract TSelf operator +(TSelf left, TSelf right);
}

public readonly record struct Distance(int Meters) : IAddable<Distance>
{
    public static Distance operator +(Distance left, Distance right) => new(left.Meters + right.Meters);
}

public static class Program
{
    private static T Sum<T>(T left, T right) where T : IAddable<T>
    {
        return left + right;
    }

    public static void Main()
    {
        var combined = Sum(new Distance(150), new Distance(275));
        Console.WriteLine(combined.Meters); // 425
    }
}

Read full guide →

String interpolation

C# 6.0 NETFx 4.6

Use string interpolation (`$"..."`) to build readable strings without manual concatenation.

From string.Format to interpolation

C# 6.0 interpolation replaces positional placeholders with inline expressions for clearer templates.

Before

var userName = "Avery";
var orderTotal = 27.5m;

var message = string.Format(
    "Customer {0} placed an order totaling {1:C}.",
    userName,
    orderTotal
);

After

var userName = "Avery";
var orderTotal = 27.5m;

var message = $"Customer {userName} placed an order totaling {orderTotal:C}.";

Formatting and alignment in interpolated strings

Interpolation supports the same numeric/date format and alignment components used with composite formatting.

var serviceName = "Billing";
var elapsedMs = 17.234;
var startedAt = new DateTime(2026, 06, 17, 11, 19, 0, DateTimeKind.Utc);

var logLine = $"{serviceName,-12} | {elapsedMs,8:F2} ms | {startedAt:O}";

Read full guide →

Switch Expressions

C# 8.0 .NET 5.0

Switch expressions bring functional-style pattern matching to C# as lightweight expressions that replace traditional switch statements with cleaner, more concise code.

Basic switch expression

Switch expressions provide a concise, expression-based alternative to traditional switch statements, reducing boilerplate and improving readability.

// Traditional switch statement
string GetDayTypeOld(DayOfWeek day)
{
    switch (day)
    {
        case DayOfWeek.Saturday:
        case DayOfWeek.Sunday:
            return "Weekend";
        default:
            return "Weekday";
    }
}

// Switch expression (C# 8.0)
string GetDayType(DayOfWeek day) =>
    day switch
    {
        DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
        _ => "Weekday"
    };

Console.WriteLine(GetDayType(DayOfWeek.Monday));    // "Weekday"
Console.WriteLine(GetDayType(DayOfWeek.Saturday));  // "Weekend"

Switch expressions with patterns

Combine switch expressions with C# pattern matching for powerful, declarative logic without traditional statement syntax.

using System;

// Pattern matching in switch expressions
public class Animal { }
public class Dog : Animal { public string Breed { get; set; } = string.Empty; }
public class Cat : Animal { public string Color { get; set; } = string.Empty; }

public static class Program
{
    private static string DescribeAnimal(Animal animal) =>
        animal switch
        {
            Dog { Breed: "Labrador" } => "Friendly Labrador",
            Dog d => $"Dog of breed {d.Breed}",
            Cat { Color: "Orange" } => "Orange tabby cat",
            Cat => "A cat",
            _ => "Unknown animal"
        };

    // Using guards (when clauses)
    private static int Classify(int number) =>
        number switch
        {
            < 0 => -1,
            0 => 0,
            > 0 and < 10 => 1,
            >= 10 and < 100 => 2,
            _ => 3
        };

    public static void Main()
    {
        var dog = new Dog { Breed = "Labrador" };
        Console.WriteLine(DescribeAnimal(dog));  // "Friendly Labrador"
        Console.WriteLine(Classify(42));         // 2
    }
}

Read full guide →

Top-level statements

C# 9.0 .NET 5.0

Write C# code at the file level without wrapping in a class and Main method, making simple programs feel less boilerplate-heavy.

Simplified hello world program

Top-level statements eliminate boilerplate for simple console applications, making the code more direct.

Before

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello, World!");
    }
}

After

Console.WriteLine("Hello, World!");

Script-like argument parsing

Top-level statements can access command-line args directly, making simple CLI scripts feel more natural.

var name = args.Length > 0 ? args[0] : "World";
Console.WriteLine($"Hello, {name}!");
Console.WriteLine($"You provided {args.Length} argument(s).");

Read full guide →

Tuples and Deconstruction

C# 7.0 NETFx 4.7

Tuples provide a lightweight syntax for grouping related values together and deconstruction enables you to unpack tuple elements into individual variables.

Creating and unpacking tuples

Tuples allow you to group multiple values together and deconstruct them into individual variables with a single expression.

// Create a tuple
(string city, decimal temperature) reading = ("Seattle", 9.5m);

// Deconstruct the tuple
var (city, temp) = reading;
Console.WriteLine($"{city}: {temp}°C");

// Tuples with named elements
var person = (name: "Alice", age: 30, city: "NYC");
var (personName, personAge, personCity) = person;

// Tuple without named elements
var coordinates = (x: 10, y: 20);
var (x, y) = coordinates;

Returning multiple values from methods

Use tuples to return multiple values from a method without creating a separate class or using out parameters.

// Method returning multiple values
(string status, int errorCode) ValidateUser(string email)
{
    if (string.IsNullOrEmpty(email))
        return ("Invalid email", 400);
    
    if (!email.Contains("@"))
        return ("Email format incorrect", 422);
    
    return ("Valid", 200);
}

// Calling the method and deconstructing
var (status, code) = ValidateUser("user@example.com");
Console.WriteLine($"Status: {status}, Code: {code}");

// Or keep as tuple
var result = ValidateUser("invalid");
Console.WriteLine($"Status: {result.status}, Code: {result.errorCode}");

Read full guide →

Using Declarations

C# 8.0 NETCore 3.0

Dispose resources automatically with less indentation and cleaner control flow than traditional using blocks.

Traditional using block vs using declaration

Using declarations keep disposal guarantees while reducing block nesting.

Before

using System;

public sealed class ScopeLogger : IDisposable
{
    private readonly string _name;

    public ScopeLogger(string name)
    {
        _name = name;
        Console.WriteLine("Opened " + _name);
    }

    public void Dispose()
    {
        Console.WriteLine("Disposed " + _name);
    }
}

public static class Program
{
    public static void Main()
    {
        using (var logger = new ScopeLogger("outer"))
        {
            Console.WriteLine("Working inside using block");
        }
    }
}

After

using System;

public sealed class ScopeLogger : IDisposable
{
    private readonly string _name;

    public ScopeLogger(string name)
    {
        _name = name;
        Console.WriteLine("Opened " + _name);
    }

    public void Dispose()
    {
        Console.WriteLine("Disposed " + _name);
    }
}

public static class Program
{
    public static void Main()
    {
        using var logger = new ScopeLogger("outer");
        Console.WriteLine("Working with flatter structure");
    }
}

Multiple resources in one scope

Each using declaration is disposed automatically at the end of the current scope.

using System;

public sealed class ScopeLogger : IDisposable
{
    private readonly string _name;

    public ScopeLogger(string name)
    {
        _name = name;
        Console.WriteLine("Opened " + _name);
    }

    public void Dispose()
    {
        Console.WriteLine("Disposed " + _name);
    }
}

public static class Program
{
    public static void Main()
    {
        using var first = new ScopeLogger("first");
        using var second = new ScopeLogger("second");
        Console.WriteLine("Do work with two resources");
    }
}

Read full guide →

Using static directives

C# 6.0 NETFx 4.6

`using static` imports static members so formulas and focused helper APIs read without type-name repetition.

Clean up dense math expressions

`using static` can make formulas and constant-heavy code easier to scan when the containing type is obvious from context.

using System;
using static System.Math;

public static class Program
{
    public static void Main()
    {
        double a = 3;
        double b = 4;
        double hypotenuse = Sqrt((a * a) + (b * b));

        Console.WriteLine(hypotenuse);
    }
}

Import a focused helper API

The feature also works with your own static helper classes, which can make validation or DSL-like code read more naturally.

using System;
using static Guard;

public static class Program
{
    public static void Main()
    {
        string userName = "Ada";
        int retryCount = 42;

        EnsureHasValue(userName, "userName");
        EnsurePositive(retryCount, "retryCount");
        Console.WriteLine("Validation passed.");
    }
}

public static class Guard
{
    public static void EnsureHasValue(string value, string parameterName)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException("A value is required.", parameterName);
        }
    }

    public static void EnsurePositive(int value, string parameterName)
    {
        if (value <= 0)
        {
            throw new ArgumentOutOfRangeException(parameterName, "The value must be positive.");
        }
    }
}

Read full guide →

Implicitly typed locals (`var`)

C# 3.0 NETFx 3.5

Use `var` for obvious local types and anonymous types while keeping compile-time static typing.

Explicit type vs var for obvious locals

When the right side already shows the concrete type, var removes duplication while keeping compile-time typing.

Before

Dictionary<string, List<decimal>> ordersByCustomer = new Dictionary<string, List<decimal>>();

After

var ordersByCustomer = new Dictionary<string, List<decimal>>();

Anonymous type projection

Anonymous types have no type name, so var is the canonical declaration style.

using System.Linq;

var metrics = new[]
{
    new { Endpoint = "/health", DurationMs = 12 },
    new { Endpoint = "/orders", DurationMs = 43 }
};

var slowEndpoints = metrics
    .Where(metric => metric.DurationMs > 20)
    .Select(metric => new { metric.Endpoint, metric.DurationMs });

Read full guide →

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.

Clone and update immutable records

Use with expressions to copy a record and change only the fields that need to move forward.

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}");
    }
}

Represent state transitions without mutation

Model workflow steps as clear transitions by producing new values instead of mutating existing instances.

using System;

public record TicketStatus(string Id, string Stage, string AssignedTo, DateTime UpdatedUtc);

public static class Program
{
    public static void Main()
    {
        var opened = new TicketStatus("T-1024", "Open", "unassigned", DateTime.UtcNow);
        var triaged = opened with { Stage = "Triaged", AssignedTo = "kat" };
        var inProgress = triaged with { Stage = "In Progress", UpdatedUtc = DateTime.UtcNow };

        Console.WriteLine(opened);
        Console.WriteLine(triaged);
        Console.WriteLine(inProgress);
    }
}

Read full guide →