Organized starting points for content that can grow into full feature guides.
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` 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 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 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 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.
public class PersonOld
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
public class PersonNew
{
public string Name { get; set; }
}
Read full guide →
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
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
Span<int> values = new Span<int>(new int[] { 10, 20, 30 });
After
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
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];
int[] combined = [..first, ..second, ..third];
Console.WriteLine(string.Join(", ", combined));
Read full guide →
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 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 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.
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; }
}
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;
public int ItemCount => _items.Length;
public int AccessCount => _accessCount;
public int Value { get; set; }
public string this[int index]
{
get => _items[index];
set => _items[index] = value;
}
public char this[int index, int charIndex]
=> _items[index][charIndex];
public string GetItem(int index)
{
_accessCount++;
return _items[index];
}
public IEnumerable<string> SortedItems => _items.OrderBy(x => x);
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; }
public Point(double x, double y) => (X, Y) = (x, y);
public double Distance() => Math.Sqrt(X * X + Y * Y);
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);
public bool IsOrigin => X == 0 && Y == 0;
public (double x, double y) AsAngle() => (
X / Distance(),
Y / Distance()
);
public static explicit operator Point(string coords)
{
var parts = coords.Split(',');
return new Point(double.Parse(parts[0]), double.Parse(parts[1]));
}
public override string ToString() => $"({X}, {Y})";
}
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 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 (`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 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.
namespace MyApp.Services;
public class UserService
{
public void PrintUser(string name)
{
Console.WriteLine($"User: {name}");
}
}
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.
namespace MyApp.Services;
public class UserService
{
public void PrintUser(string name)
{
Console.WriteLine($"User: {name}");
}
}
Read full guide →
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);
Console.WriteLine(product);
}
}
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 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.
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Text.Json;
global using System.Threading.Tasks;
namespace MyApp;
public class UserService
{
public async Task<List<User>> GetUsersAsync()
{
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.
global using System;
global using static System.Console;
global using static System.Math;
namespace MyApp;
public class Calculator
{
public double CalculateHypotenuse(double a, double b)
{
double result = Sqrt(Pow(a, 2) + Pow(b, 2));
WriteLine($"Hypotenuse: {result}");
return result;
}
}
Read full guide →
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 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 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 →
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 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.
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);
}
}
public class OrderProcessorNew
{
public void ProcessOrder(Order order)
{
ValidateOrder(order);
CalculateTotal(order);
ShipOrder(order);
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");
}
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
{
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);
}
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;
}
}
public Func<int, int> CreateMultiplier(int factor)
{
int Multiply(int x) => x * factor;
return Multiply;
}
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}");
}
}
public class TreeNode
{
public int Value { get; set; }
public TreeNode Left { get; set; }
public TreeNode Right { get; set; }
}
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
{
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();
}
}
public IEnumerable<int> GenerateSequence(int count)
{
return GetNumbers();
IEnumerable<int> GetNumbers()
{
for (int i = 0; i < count; i++)
{
yield return i * 2;
}
}
}
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);
yield return i;
}
}
}
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);
}
}
}
}
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);
Console.WriteLine($"Processed: {item}");
}
}
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 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 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 →
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.
public class UserProfileOld
{
public string? GetDisplayName(User? user)
{
if (user == null)
return "Unknown";
return user.Name;
}
}
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;
public Dictionary<string, string> GetCacheOld()
{
if (_cache == null)
{
_cache = new Dictionary<string, string>();
}
return _cache;
}
public Dictionary<string, string> GetCacheNew()
{
_cache ??= new Dictionary<string, string>();
return _cache;
}
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; }
}
public string GetTheme(UserPreferences? prefs, string systemDefault, string hardcodedDefault)
{
return prefs?.Theme ?? systemDefault ?? hardcodedDefault;
}
public class ApiResponse
{
public string? Message { get; set; }
public string? ErrorCode { get; set; }
}
public string GetUserMessage(ApiResponse? response, string requestId)
{
return response?.Message ?? response?.ErrorCode ?? $"Request {requestId} failed";
}
public int? GetConfigValue(Dictionary<string, int?>? configs, string key, int? defaultValue)
{
return configs?[key] ?? defaultValue ?? 0;
}
}
Read full guide →
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 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 →
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);
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 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 →
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 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.
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);
}
}
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 };
int[] firstThree = numbers[..3];
int[] lastThree = numbers[5..];
int[] middle = numbers[2..5];
int[] copy = numbers[..];
}
public void StringRanges()
{
string path = "C:\\Users\\Documents\\file.txt";
int lastSlash = path.LastIndexOf('\\');
string fileName = path[(lastSlash + 1)..];
int lastDot = path.LastIndexOf('.');
string extension = path[lastDot..];
string nameOnly = path[..lastDot];
}
public void SpanRanges()
{
Span<byte> buffer = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
Span<byte> header = buffer[..2];
Span<byte> payload = buffer[2..];
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;
var page = allItems[(pageNumber * pageSize)..((pageNumber + 1) * pageSize)];
}
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"
};
var mainContent = logLines[1..^1];
var recent = logLines[^3..];
var criticalLines = logLines[4..5];
}
public void DataValidationExample()
{
string csvLine = "123,John,Doe,john@example.com,2025-01-15";
var fields = csvLine.Split(',');
string email = fields[^2];
string dateStr = fields[^1];
string fullName = string.Join(" ", fields[1..3]);
bool valid = fields.Length >= 5;
}
public void SlidingWindowExample()
{
var values = new[] { 10, 20, 30, 40, 50, 60, 70, 80 };
int windowSize = 3;
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();
}
}
}
Read full guide →
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.
string jsonOld = "{\"name\": \"Alice\", \"age\": 30}";
string jsonNew = """
{
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
""";
string sqlQuery = """
SELECT u.Id, u.Name, u.Email
FROM Users u
WHERE u.Status = 'Active'
ORDER BY u.Name
""";
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.
string name = "Alice";
int age = 30;
string email = "alice@example.com";
string jsonResponse = $$"""
{
"name": "{{name}}",
"age": {{age}},
"email": "{{email}}"
}
""";
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 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 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 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 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;
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()
{
var user = new User
{
Id = 1,
Email = "alice@example.com",
FirstName = "Alice",
LastName = "Smith"
};
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;
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()
{
var product1 = new Product
{
Name = "Laptop",
Price = 999.99m,
Description = "High-performance laptop"
};
Console.WriteLine($"Product: {product1.Name}, Price: {product1.Price}");
}
}
Read full guide →
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));
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());
Console.WriteLine(remainder.ToString());
Read full guide →
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);
}
}
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);
}
}
Read full guide →
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 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.
string GetDayTypeOld(DayOfWeek day)
{
switch (day)
{
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
return "Weekend";
default:
return "Weekday";
}
}
string GetDayType(DayOfWeek day) =>
day switch
{
DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
_ => "Weekday"
};
Console.WriteLine(GetDayType(DayOfWeek.Monday));
Console.WriteLine(GetDayType(DayOfWeek.Saturday));
Switch expressions with patterns
Combine switch expressions with C# pattern matching for powerful, declarative logic without traditional statement syntax.
using System;
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"
};
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));
Console.WriteLine(Classify(42));
}
}
Read full guide →
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 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.
(string city, decimal temperature) reading = ("Seattle", 9.5m);
var (city, temp) = reading;
Console.WriteLine($"{city}: {temp}°C");
var person = (name: "Alice", age: 30, city: "NYC");
var (personName, personAge, personCity) = person;
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.
(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);
}
var (status, code) = ValidateUser("user@example.com");
Console.WriteLine($"Status: {status}, Code: {code}");
var result = ValidateUser("invalid");
Console.WriteLine($"Status: {result.status}, Code: {result.errorCode}");
Read full guide →
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` 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 →
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 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 →
No features match the selected filters. Try a different mode or version.