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 withReleasedDefault forHeadline features
10.NET 6November 2021net6.0global usings, file-scoped namespaces, record structs
11.NET 7November 2022net7.0raw strings, required, list patterns, generic math
12.NET 8November 2023net8.0primary constructors, collection expressions
13.NET 9November 2024net9.0params collections, Lock, partial properties
14.NET 10November 2025net10.0extension members, field, ?.= assignment

The lookups people make most:

I want to…WriteSince
Drop the namespace indentnamespace Store.Api.Orders;10
Import a namespace once per projectglobal using Store.Api.Orders;10
Paste JavaScript Object Notation (JSON) or SQL without escaping"""...""" and $$"""...{{x}}..."""11
Force callers to set a propertypublic required string Name { get; init; }11
Inject dependencies without fieldsclass OrderService(AppDbContext db)12
Create an array or listint[] ids = [1, 2, 3]; and [.. a, .. b]12
Accept many arguments without an arrayparams ReadOnlySpan<string> tags13
Lock without new object()private readonly Lock _gate = new();13
Validate in a setter, no backing fieldset => field = value.Trim();14
Add a property to a type you do not ownextension(Product p) { public bool InStock => ... }14
Assign only when the target is not nullorder?.Status = OrderStatus.Paid;14
Get "List" from a generic typenameof(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 &lt;VIP&gt;</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