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*.*
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 fragile LangVersion overrides.
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.
The lookups people make most:
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.
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.
Global usings. Import a namespace once for the whole project.
// Before: repeated at the top of every fileusing 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.
// Beforenamespace Store.Api.Orders{ public sealed class OrderService { }}
// Afternamespace 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// Afterpublic readonly record struct Money(decimal Amount, string Currency);
js
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.
// Beforeif (order is { Lines: { Count: > 0 } }) { }
// Afterif (order is { Lines.Count: > 0 }) { }
Constant interpolated strings. Build const strings, for example route templates, with $"".
// Beforepublic const string Products = Base + "/products";
js
// Afterpublic 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.
// BeforeFunc<string, int> parse = s => int.Parse(s);
js
// Aftervar parse = (string s) => int.Parse(s); // inferred as Func<string, int>var pick = object (bool b) => b ? 1 : "one"; // explicit return typeapp.MapGet("/api/health", [AllowAnonymous] () => Results.Ok()); // Microsoft.AspNetCore.Authorization
Raw string literals. Multi-line text with quotes and braces, no escaping.
// Beforevar json = "{ \"id\": " + id + ", \"name\": \"Mouse\" }";
bash
// After: $$ means {{x}} interpolates and a single { is literalvar 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; }}
js
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) ...// Afterstatic 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>), ...// Afterstatic 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.
// Beforebyte[] ok = Encoding.UTF8.GetBytes("OK");
// AfterReadOnlySpan<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// Afterfile 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.
Primary constructors for classes. Constructor parameters in the class header, in scope everywhere.
// Beforepublic sealed class PriceCalculator{ private readonly decimal _vatRate; public PriceCalculator(decimal vatRate) => _vatRate = vatRate;}
// Afterpublic 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.
// Beforevar ids = new int[] { 1, 2, 3 };var tags = new List<string> { "new", "sale" };var none = Array.Empty<Product>();
// Afterint[] ids = [1, 2, 3];List<string> tags = ["new", "sale"];string[] all = [.. tags, "featured"]; // new, sale, featuredProduct[] none = [];
Default lambda parameters. Optional parameters on lambdas.
// Before: a local function or two overloads// Aftervar discount = (decimal price, decimal rate = 0.10m) => price * (1 - rate);discount(100m); // 90.00discount(100m, 0.25m); // 75.00
Alias any type. using aliases for tuples, arrays, and pointer types.
// Before: only named types could be aliased// Afterusing 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;}
js
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.
params collections. params works with spans and collection interfaces, not only arrays.
// Before: allocates an array on every callpublic static string Tags(params string[] tags) => string.Join("|", tags);
js
// Afterpublic 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.
// Beforeprivate readonly object _gate = new();
// Afterprivate readonly Lock _gate = new();
lock (_gate) // compiles to _gate.EnterScope(){ _reserved += qty;}
\e escape. The ESCAPE character (U+001B) for terminal colors.
// BeforeConsole.WriteLine("\u001b[32mPaid\u001b[0m");
// AfterConsole.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 } }// Aftervar 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;
js
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;}
js
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.
Extension members. Extension blocks add properties, methods, and static members to types you do not own.
// Before: extension methods onlypublic static bool InStock(this Product p) => p.Stock > 0;
// Afterpublic static class ProductExtensions{ extension(Product product) { public bool InStock => product.Stock > 0; public decimal PriceWithVat(decimal rate) => product.Price * (1 + rate); }
js
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.
// Beforeprivate string _email = "";public string Email{ get => _email; set => _email = value.Trim().ToLowerInvariant();}
js
// Afterpublic 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.
// Beforeif (order is not null) order.Status = OrderStatus.Paid;
// Afterorder?.Status = OrderStatus.Paid; // right side runs only when order is not nullorder?.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.Total();// C# 13: error CS1929: 'decimal[]' does not contain a definition for 'Total' ...// C# 14: compiles and returns 30
nameof with unbound generics. No dummy type argument needed.
// Beforenameof(List<int>) // "List"
// Afternameof(List<>) // "List"nameof(Dictionary<,>) // "Dictionary"
Simple lambda parameters with modifiers. out, ref, in, scoped without spelling out types (params still needs them).
public delegate bool TryParser<T>(string text, out T result);
js
// BeforeTryParser<decimal> p1 = (string text, out decimal result) => decimal.TryParse(text, out result);
js
// AfterTryParser<decimal> p2 = (text, out result) => decimal.TryParse(text, out result);
Partial constructors and partial events. One defining and one implementing declaration, like partial methods.
public partial class Cart{ public partial Cart(int id); public partial event EventHandler? Changed;}
public partial class Cart{ private EventHandler? _changed; public int Id { get; } public partial Cart(int id) { Id = id; } public partial event EventHandler? Changed { add => _changed += value; remove => _changed -= value; }}
User-defined compound assignment. Overload += and ++ as instance operators that mutate in place.
// Before: static operator + creates a new object on every +=// Afterpublic sealed class Basket{ public decimal Total { get; private set; } public int Items { get; private set; }
public void operator +=(decimal price) { Total += price; Items++; } public void operator ++() => Items++;}
js
var basket = new Basket();basket += 50m; basket += 25m; basket++; // Total 75, Items 3
C# 14 also adds preprocessor directives for file-based apps (dotnet run app.cs).
Solution-wide defaults. Put this next to your .sln; keep global usings in Store.Api.csproj so test projects still build.
<!-- File: Directory.Build.props --><Project> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <!-- No LangVersion: net10.0 gives you C# 14 --> </PropertyGroup></Project>
php
<!-- File: Store.Api.csproj (inside <Project>) --><ItemGroup> <Using Include="Store.Api.Orders" /> <Using Include="Store.Api.Products" /></ItemGroup>
A request data transfer object (DTO) with required and field validation.
// File: Products/CreateProductRequest.csnamespace Store.Api.Products;
public sealed class CreateProductRequest{ public required string Name { get; init => field = value.Trim(); }
js
public required decimal Price { get; init => field = value >= 0m ? value : throw new ArgumentOutOfRangeException(nameof(Price), "Price cannot be negative."); }
public int Stock { get; init; }}
A service with a primary constructor, collection expressions, and patterns.
// File: Orders/OrderPricing.csnamespace Store.Api.Orders;
public sealed class OrderPricing(TimeProvider clock){ private static readonly HashSet<DayOfWeek> Weekend = [DayOfWeek.Saturday, DayOfWeek.Sunday];
js
public decimal ShippingFee(Order order) => order switch { { Lines.Count: 0 } => 0m, { Total: >= 500m } => 0m, _ when Weekend.Contains(clock.GetUtcNow().DayOfWeek) => 29.90m, _ => 49.90m };
js
public static int[] ProductIds(Order order) => [.. order.Lines.Select(l => l.ProductId).Distinct()];}
Register it with builder.Services.AddSingleton(TimeProvider.System); and builder.Services.AddScoped<OrderPricing>();.
An HTML email body with a raw interpolated string.
// File: Infrastructure/Email/OrderEmailTemplates.csusing System.Globalization;using System.Net;using Store.Api.Orders;
namespace Store.Api.Infrastructure.Email;
public static class OrderEmailTemplates{ private static readonly CultureInfo Turkish = CultureInfo.GetCultureInfo("tr-TR");
js
public static string Confirmation(Order order, string customerName) => $$""" <h1>Thank you, {{WebUtility.HtmlEncode(customerName)}}</h1> <p>Order #{{order.Id}} is {{order.Status}}.</p> <p>Total: {{order.Total.ToString("C", Turkish)}}</p> """;}
Output for order 1001 with a total of 1200 and the name Ayşe <VIP>:
<h1>Thank you, Ayşe <VIP></h1><p>Order #1001 is Paid.</p><p>Total: ₺1.200,00</p>
Pass the result to IEmailService.SendAsync(to, subject, htmlBody, ct).
Tags: csharp, dotnet, csharp-14, dotnet-10, cheat-sheet
Originally published at www.softinbit.com*.*
New articles by email: subscribe*.*
Modern C# Cheat Sheet: C# 10 to C# 14 was originally published in Dev Genius on Medium, where people are continuing the conversation by highlighting and responding to this story.