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.
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. default ignores the target framework, so it is not the same as leaving LangVersion out.- To see what you are compiling with, put
#error version in any file:
1
| 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):
1
| 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.
1
2
3
4
5
| // 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.
1
2
3
4
5
6
7
8
9
10
| // 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.
1
2
3
4
5
6
7
8
| // 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.
1
2
3
4
5
| // 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 $"".
1
2
3
4
5
6
| // 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.
1
2
3
4
5
6
7
| // 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.
1
2
3
4
5
6
7
8
| // 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.
1
2
3
4
5
6
7
8
| 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”.
1
2
3
4
5
6
7
8
9
| // 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.
1
2
3
4
5
6
7
8
9
10
11
| 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.
1
2
3
4
5
| // 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.
1
2
3
4
5
6
| // 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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.
1
2
3
4
5
6
7
8
9
10
11
12
| // 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.
1
2
3
4
5
6
7
8
9
10
| // 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.
1
2
3
4
5
| // 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.
1
2
3
4
5
6
| // 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.
1
2
3
4
5
6
7
8
9
10
| 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.
1
2
3
4
5
6
7
8
9
10
| 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.
1
2
3
4
5
6
7
| // 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.
1
2
3
4
5
6
7
8
9
10
| // 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.
1
2
3
4
5
| // 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).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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.
1
2
3
4
5
| 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.
1
2
3
4
5
6
7
8
9
10
| 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.
1
2
3
4
5
6
7
8
9
10
| 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 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.
1
2
3
4
5
6
7
| // 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.
1
2
3
4
5
6
7
8
9
10
11
| 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.
1
2
3
4
5
6
| // Before
nameof(List<int>) // "List"
// After
nameof(List<>) // "List"
nameof(Dictionary<,>) // "Dictionary"
|
Simple lambda parameters with modifiers. out, ref, in, scoped without spelling out types (params still needs them).
1
2
3
4
5
6
7
| public delegate bool TryParser<T>(string text, out T result);
// Before
TryParser<decimal> p1 = (string text, out decimal result) => decimal.TryParse(text, out result);
// After
TryParser<decimal> p2 = (text, out result) => decimal.TryParse(text, out result);
|
Partial constructors and partial events. One defining and one implementing declaration, like partial methods.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Before: static operator + creates a new object on every +=
// After
public 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++;
}
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).
Gotchas#
field is now a keyword inside property accessors. A class with a member named field gets warning CS9258; write @field or this.field for the old member.- Primary constructor parameters are mutable captured state, not
readonly fields. Using one both to initialize a field and directly in a method gives warning CS9124 (two copies). var x = [1, 2]; fails with CS9176 “There is no target type for the collection expression.” Give the variable a type.- Casting a
Lock to object silently falls back to Monitor locking; the compiler warns with CS9216. [Experimental] produces an error, not a warning, until the caller suppresses that exact ID.required is a compile-time check on object initializers, not input validation.- An instance member always wins over an extension member with the same name.
?.= skips the right side entirely when the target is null, including its side effects.
Copy-paste snippets#
Solution-wide defaults. Put this next to your .sln; keep global usings in Store.Api.csproj so test projects still build.
1
2
3
4
5
6
7
8
9
| <!-- 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>
|
1
2
3
4
5
| <!-- 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // File: Products/CreateProductRequest.cs
namespace Store.Api.Products;
public sealed class CreateProductRequest
{
public required string Name
{
get;
init => field = value.Trim();
}
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // File: Orders/OrderPricing.cs
namespace Store.Api.Orders;
public sealed class OrderPricing(TimeProvider clock)
{
private static readonly HashSet<DayOfWeek> Weekend = [DayOfWeek.Saturday, DayOfWeek.Sunday];
public decimal ShippingFee(Order order) => order switch
{
{ Lines.Count: 0 } => 0m,
{ Total: >= 500m } => 0m,
_ when Weekend.Contains(clock.GetUtcNow().DayOfWeek) => 29.90m,
_ => 49.90m
};
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // File: Infrastructure/Email/OrderEmailTemplates.cs
using 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");
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>:
1
2
3
| <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).
- “C# Pattern Matching Cheat Sheet”
- “async/await Rules Cheat Sheet”
- “Upgrading a Production API from .NET 8 to .NET 10: What Broke”
- “C# Types in Practice: Value vs Reference, decimal vs double, and Nullable” (Zero to Full-Stack .NET)
- “Methods, Parameters, Tuples, and Extension Methods” (Zero to Full-Stack .NET)
Tags: csharp, dotnet, csharp-14, dotnet-10, cheat-sheet