Modern C# Cheat Sheet: C# 10 to C# 14
Note: this article was drafted with AI assistance and reviewed by Softinbit Technology before publishing. It is part of the series The .NET Field Guide.
Every language feature from .NET 6 to .NET 10, each with a one-line purpose and a before/after snippet.
Part of "The .NET Field Guide". Section: Cheat Sheets.
TL;DR
- Every C# feature from C# 10 (.NET 6) through C# 14 (.NET 10) condensed into single-purpose before/after snippets tested on .NET 10 SDK (10.0.300).
- Fast lookups for top idioms: file-scoped namespaces, raw strings, required properties, primary constructors, collection expressions, Lock, and field-backed properties.
- Clear guidance on target framework defaults and compiler diagnostics (
#error version) without fragileLangVersionoverrides.Level: Intermediate · Prerequisites: Working knowledge of C# 9 / .NET 5 or earlier · Reading time: 13 min
Your team just moved Store.Api to .NET 10, and a reviewer comments "use field here" on a property you wrote the C# 9 way. This sheet is for developers who learned C# a few versions ago and want the short version of C# 10 (.NET 6) through C# 14 (.NET 10). Every snippet was compiled with the .NET 10 software development kit (SDK), version 10.0.300; outputs and error messages are real.
At a glance
| C# | Ships with | Released | Default for | Headline features |
|---|---|---|---|---|
| 10 | .NET 6 | November 2021 | net6.0 | global usings, file-scoped namespaces, record structs |
| 11 | .NET 7 | November 2022 | net7.0 | raw strings, required, list patterns, generic math |
| 12 | .NET 8 | November 2023 | net8.0 | primary constructors, collection expressions |
| 13 | .NET 9 | November 2024 | net9.0 | params collections, Lock, partial properties |
| 14 | .NET 10 | November 2025 | net10.0 | extension members, field, ?.= assignment |
The lookups people make most:
| I want to... | Write | Since |
|---|---|---|
| Drop the namespace indent | namespace Store.Api.Orders; | 10 |
| Import a namespace once per project | global using Store.Api.Orders; | 10 |
| Paste JavaScript Object Notation (JSON) or SQL without escaping | """...""" and $$"""...{{x}}...""" | 11 |
| Force callers to set a property | public required string Name { get; init; } | 11 |
| Inject dependencies without fields | class OrderService(AppDbContext db) | 12 |
| Create an array or list | int[] ids = [1, 2, 3]; and [.. a, .. b] | 12 |
| Accept many arguments without an array | params ReadOnlySpan<string> tags | 13 |
Lock without new object() | private readonly Lock _gate = new(); | 13 |
| Validate in a setter, no backing field | set => field = value.Trim(); | 14 |
| Add a property to a type you do not own | extension(Product p) { public bool InStock => ... } | 14 |
| Assign only when the target is not null | order?.Status = OrderStatus.Paid; | 14 |
Get "List" from a generic type | nameof(List<>) | 14 |
Which version you get (LangVersion)
The compiler picks the default C# version from your target framework (the "Default for" column above). You do not need LangVersion in the project file.
- Microsoft's documentation states that using a C# version newer than the default for your target framework is not supported. Upgrade the target framework instead.
- Do not set
<LangVersion>latest</LangVersion>. Its meaning changes with whatever SDK is installed on each machine. -
defaultignores the target framework, so it is not the same as leavingLangVersionout. - To see what you are compiling with, put
#error versionin any file:
error CS8304: Compiler version: '5.6.0-2.26230.102 (caa81fa4...)'. Language version: 14.0. Using a C# 14 feature when the language version is 13 (for example net9.0 built with the .NET 10 SDK):
error CS9260: Feature 'field keyword' is not available in C# 13.0. Please use language version 14.0 or greater. C# 10 (.NET 6)
Global usings. Import a namespace once for the whole project.
// Before: repeated at the top of every file using Store.Api.Orders; // After: once, in GlobalUsings.cs (or <Using Include="Store.Api.Orders" /> in the .csproj) global using Store.Api.Orders; File-scoped namespaces. One less indentation level per file.
// Before namespace Store.Api.Orders { public sealed class OrderService { } } // After namespace Store.Api.Orders; public sealed class OrderService { } Record structs. Value-type records with generated equality, ToString, and with.
// Before: a struct with hand-written Equals, GetHashCode, ==, and ToString // After public readonly record struct Money(decimal Amount, string Currency); var a = new Money(10m, "TRY"); var b = a with { Amount = 12m }; Console.WriteLine(a); // Money { Amount = 10, Currency = TRY } Console.WriteLine(a == new Money(10m, "TRY")); // True Extended property patterns. Reach into nested members with a dot.
// Before if (order is { Lines: { Count: > 0 } }) { } // After if (order is { Lines.Count: > 0 }) { } Constant interpolated strings. Build const strings, for example route templates, with $"".
// Before public const string Products = Base + "/products"; // After public const string Base = "/api"; public const string Products = $"{Base}/products"; // "/api/products", usable in [Route(Products)] Lambda improvements. Lambdas get a natural type, an optional explicit return type, and attributes.
// Before Func<string, int> parse = s => int.Parse(s); // After var parse = (string s) => int.Parse(s); // inferred as Func<string, int> var pick = object (bool b) => b ? 1 : "one"; // explicit return type app.MapGet("/api/health", [AllowAnonymous] () => Results.Ok()); // Microsoft.AspNetCore.Authorization C# 11 (.NET 7)
Raw string literals. Multi-line text with quotes and braces, no escaping.
// Before var json = "{ \"id\": " + id + ", \"name\": \"Mouse\" }"; // After: $$ means {{x}} interpolates and a single { is literal var json = $$""" { "id": {{id}}, "name": "Mouse" } """; // { "id": 42, "name": "Mouse" } Required members. The compiler forces object initializers to set these properties.
public sealed class CreateProductRequest { public required string Name { get; init; } public required decimal Price { get; init; } } var request = new CreateProductRequest { Name = "Mouse" }; // error CS9035: Required member 'CreateProductRequest.Price' must be set in the object initializer or attribute constructor. Mark a constructor that sets them all with [SetsRequiredMembers] from System.Diagnostics.CodeAnalysis.
List patterns. Match arrays and lists by shape, with .. for "the rest".
// Before: if (ids.Length == 0) ... else if (ids.Length == 1) ... // After static string Describe(int[] ids) => ids switch { [] => "empty", [var only] => $"one: {only}", [var first, .., var last] => $"{first}..{last}", }; // Describe([1, 2, 3]) -> "1..3" Generic math (static abstract interface members). Write one numeric algorithm for every number type.
using System.Numerics; // Before: Sum(IEnumerable<int>), Sum(IEnumerable<decimal>), ... // After static T Sum<T>(IEnumerable<T> values) where T : INumber<T> { T total = T.Zero; foreach (var v in values) total += v; return total; } // Sum([10.5m, 20m]) -> 30.5 Sum([1, 2, 3]) -> 6 Your own interfaces can declare static abstract TSelf Zero { get; } the same way.
UTF-8 string literals. Get bytes at compile time.
// Before byte[] ok = Encoding.UTF8.GetBytes("OK"); // After ReadOnlySpan<byte> ok = "OK"u8; // 2 bytes, no runtime encoding File-local types. A type visible only inside its own file.
// Before: internal helper visible to the whole assembly // After file sealed class SkuFormatter { public static string Format(int id) => $"SKU-{id:D5}"; // SKU-00042 } scoped. Promise that a span parameter does not escape, so callers can pass stack memory.
public ref struct ReceiptWriter { private Span<char> _dest; public int Written; public ReceiptWriter(Span<char> dest) { _dest = dest; Written = 0; } public void Append(scoped ReadOnlySpan<char> text) { text.CopyTo(_dest[Written..]); Written += text.Length; } } // Without "scoped", w.Append(stackallocBuffer) fails with error CS8350. C# 12 (.NET 8)
Primary constructors for classes. Constructor parameters in the class header, in scope everywhere.
// Before public sealed class PriceCalculator { private readonly decimal _vatRate; public PriceCalculator(decimal vatRate) => _vatRate = vatRate; } // After public sealed class PriceCalculator(decimal vatRate) { public decimal WithVat(decimal net) => net * (1 + vatRate); // 100 -> 120.00 } Collection expressions. One syntax for arrays, lists, spans, and sets, plus .. spread.
// Before var ids = new int[] { 1, 2, 3 }; var tags = new List<string> { "new", "sale" }; var none = Array.Empty<Product>(); // After int[] ids = [1, 2, 3]; List<string> tags = ["new", "sale"]; string[] all = [.. tags, "featured"]; // new, sale, featured Product[] none = []; Default lambda parameters. Optional parameters on lambdas.
// Before: a local function or two overloads // After var discount = (decimal price, decimal rate = 0.10m) => price * (1 - rate); discount(100m); // 90.00 discount(100m, 0.25m); // 75.00 Alias any type. using aliases for tuples, arrays, and pointer types.
// Before: only named types could be aliased // After using LineTotal = (int ProductId, decimal Total); LineTotal line = (5, 59.90m); Console.WriteLine(line.Total); // 59.90 Inline arrays. A fixed-size buffer inside a struct, without unsafe.
using System.Runtime.CompilerServices; [InlineArray(3)] public struct LastThreePrices { private decimal _element0; } var last = new LastThreePrices(); last[0] = 10m; last[1] = 20m; last[2] = 30m; // indexable and foreach-able Experimental attribute. Mark APIs that callers must opt into.
using System.Diagnostics.CodeAnalysis; [Experimental("STORE001")] public static class Recommendations { public static string[] For(int userId) => ["Mouse", "Keyboard"]; } // Calling it from Store.Api.Products without suppression: // error STORE001: 'Store.Api.Products.Recommendations' is for evaluation purposes only and is // subject to change or removal in future updates. Suppress this diagnostic to proceed. Suppress with #pragma warning disable STORE001 or <NoWarn>STORE001</NoWarn>. Also new in C# 12: ref readonly parameters, and interceptors as a preview feature.
C# 13 (.NET 9)
params collections. params works with spans and collection interfaces, not only arrays.
// Before: allocates an array on every call public static string Tags(params string[] tags) => string.Join("|", tags); // After public static string Tags(params ReadOnlySpan<string> tags) => string.Join("|", tags); public static int CountIds(params IEnumerable<int> ids) => ids.Count(); // Tags("new", "sale") -> "new|sale" System.Threading.Lock. A dedicated lock type; lock uses its API instead of Monitor.
// Before private readonly object _gate = new(); // After private readonly Lock _gate = new(); lock (_gate) // compiles to _gate.EnterScope() { _reserved += qty; } \e escape. The ESCAPE character (U+001B) for terminal colors.
// Before Console.WriteLine("\u001b[32mPaid\u001b[0m"); // After Console.WriteLine("\e[32mPaid\e[0m"); Partial properties and indexers. Declare in one part, implement in another (source generators use this).
public partial class ProductView { public partial string Sku { get; set; } // declaring part } public partial class ProductView { private string _sku = ""; public partial string Sku // implementing part { get => _sku; set => _sku = value.ToUpperInvariant(); } } Implicit index access in object initializers. ^ (from the end) works inside initializers.
public sealed class Shelf { public int[] Slots { get; } = new int[5]; } // Before: front indexes only, { Slots = { [4] = 99 } } // After var shelf = new Shelf { Slots = { [^1] = 99, [0] = 1 } }; // 1,0,0,0,99 OverloadResolutionPriority. Library authors steer callers to a better overload without breaking them.
using System.Runtime.CompilerServices; public static class Logging { public static string Log(string message) => "string overload"; [OverloadResolutionPriority(1)] public static string Log(ReadOnlySpan<char> message) => "span overload"; } // Logging.Log("hello") -> "span overload" ref struct interfaces and allows ref struct. Span-holding types can implement interfaces and flow through generics.
public interface ISkuSource { int Count { get; } } public ref struct SpanSkuSource : ISkuSource { private readonly ReadOnlySpan<string> _skus; public SpanSkuSource(ReadOnlySpan<string> skus) => _skus = skus; public int Count => _skus.Length; } static int CountOf<T>(T source) where T : ISkuSource, allows ref struct => source.Count; Also in C# 13: ref locals and unsafe blocks inside async methods and iterators (not across await or yield), and smarter method group overload resolution.
C# 14 (.NET 10)
Extension members. Extension blocks add properties, methods, and static members to types you do not own.
// Before: extension methods only public static bool InStock(this Product p) => p.Stock > 0; // After public static class ProductExtensions { extension(Product product) { public bool InStock => product.Stock > 0; public decimal PriceWithVat(decimal rate) => product.Price * (1 + rate); } extension(Product) { public static Product Placeholder => new() { Name = "(none)" }; } } // p.InStock -> True p.PriceWithVat(0.20m) -> 120.00 Product.Placeholder.Name -> (none) The field keyword. Use the compiler-generated backing field inside an accessor.
// Before private string _email = ""; public string Email { get => _email; set => _email = value.Trim().ToLowerInvariant(); } // After public string Email { get; set => field = value.Trim().ToLowerInvariant(); } = ""; // " Ali@Example.COM " -> "ali@example.com" Null-conditional assignment. ?. and ?[] on the left side of = and compound assignments.
// Before if (order is not null) order.Status = OrderStatus.Paid; // After order?.Status = OrderStatus.Paid; // right side runs only when order is not null order?.Total += 10m; // order?.Total++; -> error CS1059 (++ and -- are not allowed here) Implicit span conversions. Arrays convert to spans for extension receivers and generic inference.
public static decimal Total(this ReadOnlySpan<decimal> prices) { decimal sum = 0; foreach (var p in prices) sum += p; return sum; } decimal[] prices = [10m, 20m]; prices.原文:https://www.a8q9.cn/en/article/modern-c-cheat-sheet-c-10-to-c-14-e154dd.html