Skip to content

C# Cheat Sheet

This cheat sheet is based on C# 14 and .NET 10. It covers everyday language features and standard library APIs with examples.


The table lists selected additions through C# 14, the stable baseline used here. It is a feature history, not a support-lifecycle table. Preview features are excluded.

C# Released Main additions
1.0 / 1.2 2002 / 2003 Classes, structs, interfaces, delegates; refinements in 1.2
2.0 2005 Generics, nullable value types, iterators, anonymous methods
3.0 2007 LINQ, lambdas, extension methods, var, anonymous types
4.0 2010 dynamic, named and optional arguments, generic covariance and contravariance
5.0 2012 async / await, caller information
6.0 2015 String interpolation, ?., nameof, expression-bodied members
7.0 2017 Tuples and deconstruction, pattern matching, local functions, out var
7.1 2017 async Main, default literals
7.2 2017 readonly struct, ref struct, in parameters
7.3 2018 unmanaged, Enum, and Delegate constraints
8.0 2019 Nullable reference types, switch expressions, async streams, ranges, using declarations
9.0 2020 Records, init, top-level statements, target-typed new
10 2021 record struct, global using, file-scoped namespaces
11 2022 Raw strings, required, list patterns, generic math
12 2023 Collection expressions, primary constructors for classes and structs
13 2024 params collections, System.Threading.Lock support, relaxed ref struct restrictions
14 2025 Extension members, field, null-conditional assignment

Official C# version history / Feature-by-feature history

Target framework and default language version

Section titled “Target framework and default language version”

With a current SDK and no LangVersion override, the compiler selects a language version from the target framework. This mapping is separate from the original release environment of older C# versions.

Target Default C#
.NET 10 14
.NET 9 13
.NET 8 12
.NET 7 11
.NET 6 10
.NET 5 9.0
.NET Core 3.x / .NET Standard 2.1 8.0
.NET Core 2.x / .NET Standard 2.0 / .NET Framework 7.3

Prefer the target framework’s default. Setting LangVersion to latest makes the result depend on the installed compiler and does not supply missing runtime APIs. global.json selects the SDK; TargetFramework selects the target; LangVersion controls accepted syntax. Using a newer language version than the target’s supported mapping is unsupported.

Official language versioning

The table shows the Windows Visual Studio release that introduced each stable C# version. It is a historical introduction map, not a list of currently supported IDE releases. Visual Studio Code is a separate product.

C# Visual Studio at introduction
1.0 Visual Studio .NET 2002
1.2 Visual Studio .NET 2003
2.0 Visual Studio 2005
3.0 Visual Studio 2008
4.0 Visual Studio 2010
5.0 Visual Studio 2012
6.0 Visual Studio 2015
7.0 Visual Studio 2017 (15.0)
7.1 Visual Studio 2017 (15.3)
7.2 Visual Studio 2017 (15.5)
7.3 Visual Studio 2017 (15.7)
8.0 Visual Studio 2019 (16.3)
9.0 Visual Studio 2019 (16.8)
10 Visual Studio 2022 (17.0)
11 Visual Studio 2022 (17.4)
12 Visual Studio 2022 (17.8)
13 Visual Studio 2022 (17.12)
14 Visual Studio 2026 (18.0)

Later compiler versions also understand older C# syntax, but opening and building an old project still depends on its project type, target framework, and installed workloads. The year in a product name and its version number differ: Visual Studio 2022 is the 17.x family; Visual Studio 2026 is the 18.x family.

Official C# / Visual Studio history

Targeting recent .NET versions in Visual Studio

Section titled “Targeting recent .NET versions in Visual Studio”
Target Default C# Minimum Visual Studio for targeting
.NET 8 12 Visual Studio 2022 (17.8) or later
.NET 9 13 Visual Studio 2022 (17.12) or later
.NET 10 14 Visual Studio 2026 (18.0) or later

An SDK being usable by an older Visual Studio does not mean that IDE supports targeting the SDK’s newest runtime. For example, .NET 10 SDK 10.0.100 can work with Visual Studio 2022 17.14, but targeting net10.0 in Visual Studio requires 2026 18.0 or later. Also check the SDK feature band selected by global.json against the IDE’s supported SDKs. With dotnet build outside Visual Studio, the installed SDK and project settings determine the build environment.

Official SDK / MSBuild / Visual Studio compatibility

Task Earlier form Newer form Introduced
Interpolation "Hello, " + name $"Hello, {name}" 6
Assign a null fallback if (name == null) name = "guest"; name ??= "guest"; 8
Construct an object List<int> values = new List<int>(); List<int> values = new(); 9
Initialize an array int[] values = new[] { 1, 2 }; int[] values = [1, 2]; 12
Assign through a non-null receiver if (person is not null) person.Name = "Ada"; person?.Name = "Ada"; 14

Older forms remain valid. The newer syntax may reduce repetition; it does not generally imply faster execution. In the snippets below, raw strings and required need C# 11, collection expressions need C# 12, and the System.Threading.Lock example uses C# 13 with .NET 9 or later.


Install the .NET 10 SDK for your OS from the official .NET download page. The runtime alone cannot build applications. For an existing project, follow the SDK selection in global.json and the target framework in its .csproj file.

Terminal window
dotnet --version
dotnet --list-sdks
dotnet new console -n HelloCSharp --framework net10.0
cd HelloCSharp
dotnet run
dotnet build -c Release

Write this example in the console project’s Program.cs. Top-level statements let you omit an explicit Main method.

// Write to standard output.
Console.WriteLine("Hello, C#!");
Console.WriteLine($"Arguments: {args.Length}");

Unless stated otherwise, each C# block on this page can replace Program.cs independently. Examples assume a net10.0 console project with ImplicitUsings and Nullable enabled. Type declarations follow top-level statements.


Use // for line comments, /* ... */ for block comments, and /// for XML documentation comments.

Console.WriteLine(MathHelpers.Add(2, 3));
public static class MathHelpers
{
/// <summary>Adds two integers.</summary>
public static int Add(int left, int right) => left + right;
}

var infers a static type from the initializer; it does not make a variable dynamically typed. Local variables must be assigned before use.

int count = 3;
long population = 8_000_000_000L;
double ratio = 0.5;
decimal price = 1_980.50m;
bool enabled = true;
char initial = 'C';
string language = "C#";
var names = new List<string>();
const int MaxRetries = 3;
Console.WriteLine($"{count}, {population}, {ratio}, {price}, {enabled}, {initial}, {language}, {names.Count}, {MaxRetries}");
Type Purpose and notes
byte / sbyte Unsigned / signed 8-bit integers
short / ushort 16-bit integers
int / uint 32-bit integers
long / ulong 64-bit integers
nint / nuint Platform-sized integers
float / double Binary floating point; use f for float literals
decimal Decimal arithmetic; use m for literals. Precision and range are still limited
bool true / false
char One UTF-16 code unit, not necessarily a complete character
string An immutable UTF-16 string
object The common base type; assigning a value type involves boxing

Fields and array elements receive default values: 0 for numbers, false for bool, and null for reference types. A readonly field can be assigned at its declaration or in a constructor, unlike a compile-time const.


Integer division produces an integer. TryParse reports conversion failure through its return value, making it useful for external input.

Console.WriteLine(5 / 2); // 2
Console.WriteLine(5 / 2.0); // 2.5
if (int.TryParse("42", out int value))
Console.WriteLine(value);
double source = 3.9;
int truncated = (int)source;
Console.WriteLine(truncated);
try
{
int max = int.MaxValue;
Console.WriteLine(checked(max + 1));
}
catch (OverflowException)
{
Console.WriteLine("Overflow");
}

Use checked to detect overflow in integer arithmetic and conversions. Specify a culture when persisting or exchanging formatted numbers and dates.


int? is a nullable value type. string? tells the compiler that a reference may be null. Nullable reference type warnings do not add runtime checks.

string? name = null;
int? length = name?.Length;
Console.WriteLine(length ?? 0);
name ??= "guest";
if (name is { Length: > 0 })
Console.WriteLine(name.ToUpperInvariant());
int? score = null;
Console.WriteLine(score.GetValueOrDefault());

?. skips access on null, ?? supplies a fallback, and ??= assigns only on null. The null-forgiving operator ! only suppresses warnings; it does not make a value safe.


Strings are immutable. Use $"..." for interpolation, @"..." to preserve backslashes in paths, and raw string literals for multiline text.

string name = "Ada";
string path = @"C:\work\notes.txt";
string json = """
{"name":"Ada","active":true}
""";
Console.WriteLine($"Hello, {name}! {path}");
Console.WriteLine(json);
Console.WriteLine(string.Join(" | ", " alpha,beta ".Trim().Split(',')));
Console.WriteLine(string.Join(" / ", new[] { "alpha", "beta" }));
Console.WriteLine("CSharp".Contains("sharp", StringComparison.OrdinalIgnoreCase));
Console.WriteLine(name[..2]);

Specify StringComparison.Ordinal or OrdinalIgnoreCase for identifier-like comparisons. Consider culture for human-language text. StringBuilder is useful for repeated concatenation.


Use if, for, foreach, while, and do for control flow. A switch expression selects a value using patterns.

int score = 85;
if (score >= 60)
Console.WriteLine("Pass");
string grade = score switch
{
>= 90 => "A",
>= 80 => "B",
>= 60 => "C",
_ => "D"
};
Console.WriteLine(grade);
for (int i = 0; i < 3; i++)
Console.WriteLine(i);
foreach (int number in new[] { 1, 2, 3 })
{
if (number == 2) continue;
Console.WriteLine(number);
}
int remaining = 2;
while (remaining > 0) remaining--;
do { remaining++; } while (remaining < 1);
object item = "hello";
if (item is string { Length: > 0 } text)
Console.WriteLine(text);
int[] values = [1, 2, 3];
Console.WriteLine(values is [1, .., 3]);

break exits a loop or switch statement; continue advances to the next iteration. Switch statement sections normally end with break or return and do not implicitly fall through.


You can declare local functions. Use tuples for multiple results, params for a variable number of arguments, and name: value for named arguments.

Console.WriteLine(Greet(name: "Ada"));
Console.WriteLine(Sum(1, 2, 3));
var (min, max) = Bounds([8, 2, 5]);
Console.WriteLine($"{min}..{max}");
int count = 1;
Increment(ref count);
Console.WriteLine(count);
static string Greet(string name, string prefix = "Hello") => $"{prefix}, {name}";
static int Sum(params int[] values) => values.Sum();
static (int Min, int Max) Bounds(int[] values) => (values.Min(), values.Max());
static void Increment(ref int value) => value++;

ref passes an initialized variable by reference, out requires assignment by the callee, and in passes a readonly reference. Ordinary arguments pass by value; for reference types, that value is a reference.


Arrays have a fixed length. List<T> is an array-backed collection that can grow. Dictionary<TKey, TValue> stores key-value pairs, while HashSet<T> stores unique values.

int[] numbers = [10, 20, 30];
Console.WriteLine(numbers[^1]);
int[] firstTwo = numbers[..2];
List<string> names = ["Ada", "Linus"];
names.Add("Grace");
names.Remove("Linus");
Console.WriteLine($"{numbers.Length}, {firstTwo.Length}, {names.Count}");
var scores = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["Ada"] = 90
};
if (scores.TryGetValue("ada", out int score))
Console.WriteLine(score);
scores["Grace"] = 95;
foreach (var (name, points) in scores)
Console.WriteLine($"{name}: {points}");
HashSet<int> unique = [1, 1, 2];
Console.WriteLine(unique.Add(2));
Console.WriteLine(unique.Contains(1));

An array range such as numbers[..2] creates a new array. A list’s Capacity is its allocated capacity; Count is its actual element count. Ordinary collections require synchronization for concurrent mutation.


Classes are reference types. required requires initialization, and init limits property assignment to initialization.

var account = new Account("A-001") { Owner = "Ada" };
account.Deposit(100m);
Console.WriteLine($"{account.Id}: {account.Owner}, {account.Balance}");
public sealed class Account(string id)
{
public string Id { get; } = id;
public required string Owner { get; init; }
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
Balance += amount;
}
}
Modifier Accessibility
public Unrestricted, within the containing type’s accessibility
private Within the declaring type
protected Within the declaring type and derived types
internal Within the same assembly
protected internal Same assembly or derived types
private protected Declaring type and derived types in the same assembly

sealed prevents inheritance. Primary constructor parameters do not automatically become public properties on ordinary classes. required does not validate the supplied value.


A struct is a value type: assignment copies its value. Records generate value-based equality. A plain record is a reference type; a record struct is a value type.

var original = new Person("Ada", 36);
var updated = original with { Age = 37 };
Console.WriteLine(original == new Person("Ada", 36));
Console.WriteLine(updated);
var point = new Point(1, 2);
var copy = point with { X = 5 };
Console.WriteLine($"{point} / {copy}");
public record Person(string Name, int Age);
public readonly record struct Point(int X, int Y);

with makes a shallow copy. Reference-type members remain shared, so records do not guarantee deep immutability. Value types can also contain reference-type fields whose referenced objects remain shared after copying.


A class can inherit one base class and implement multiple interfaces. Interfaces express shared contracts.

IShape shape = new Circle(3);
Console.WriteLine(shape.Area);
Animal animal = new Dog();
animal.Speak();
public interface IShape
{
double Area { get; }
}
public sealed class Circle(double radius) : IShape
{
public double Area => Math.PI * radius * radius;
}
public abstract class Animal
{
public abstract void Speak();
}
public sealed class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof");
}

Use virtual for an overridable method with a base implementation and override to override it. This differs from hiding a member with new.


Enums represent named integer values. For flags, assign values to individual bits.

Access access = Access.Read | Access.Write;
Console.WriteLine(access.HasFlag(Access.Read));
Console.WriteLine((access & Access.Write) != 0);
Console.WriteLine(Enum.IsDefined(Status.Ready));
public enum Status { Unknown, Ready, Done }
[Flags]
public enum Access { None = 0, Read = 1, Write = 2, Execute = 4 }

Casting an integer can create an undefined enum value. Validate external input. Enum.IsDefined does not accept a flag combination unless that exact value is declared.


Type parameters let you reuse algorithms and data structures while retaining type safety. where constrains the supported types.

Console.WriteLine(Max(3, 7));
var box = new Box<string>("hello");
Console.WriteLine(box.Value);
static T Max<T>(T left, T right) where T : IComparable<T>
=> left.CompareTo(right) >= 0 ? left : right;
public sealed class Box<T>(T value)
{
public T Value { get; } = value;
}

Common constraints include class, struct, notnull, interfaces, and new(). The new() constraint requires a public parameterless constructor and follows other ordinary constraints.


Func<...> represents a function with a return value; Action<...> represents one without. Lambdas can capture outer variables. Events notify subscribers from a publisher.

int factor = 2;
Func<int, int> multiply = value => value * factor;
factor = 3;
Console.WriteLine(multiply(4));
Action<string> print = message => Console.WriteLine(message);
print("Ready");
var counter = new Counter();
EventHandler handler = (_, _) => Console.WriteLine("Changed");
counter.Changed += handler;
counter.Increment();
counter.Changed -= handler;
public sealed class Counter
{
public int Value { get; private set; }
public event EventHandler? Changed;
public void Increment()
{
Value++;
Changed?.Invoke(this, EventArgs.Empty);
}
}

A capture retains the variable, not a snapshot of its value. Unsubscribe when a subscription to a long-lived publisher is no longer needed. Use static on a lambda that should not capture outer state.


LINQ expresses filtering, projection, and aggregation over data sources. It supports method syntax and query syntax.

int[] numbers = [1, 2, 3, 4, 5];
var query = numbers.Where(n => n % 2 == 0).Select(n => n * 10);
int[] result = query.ToArray();
Console.WriteLine(string.Join(", ", result));
Console.WriteLine(numbers.Any(n => n > 4));
Console.WriteLine(numbers.All(n => n > 0));
Console.WriteLine(numbers.Sum());
Console.WriteLine(numbers.FirstOrDefault(n => n > 10, -1));
var descending = from n in numbers
where n >= 3
orderby n descending
select n;
Console.WriteLine(string.Join(", ", descending));
foreach (var group in numbers.GroupBy(n => n % 2))
Console.WriteLine($"{group.Key}: {group.Count()}");
Operation Common methods
Filter and project Where, Select, SelectMany
Sort OrderBy, OrderByDescending, ThenBy
First, last, single First, Last, Single and their OrDefault variants
Limit results Take, Skip, TakeWhile
Aggregate Count, Sum, Average, Min, Max, Aggregate
Set operations Distinct, Union, Intersect, Except
Materialize ToArray, ToList, ToDictionary

Where and Select usually run when enumerated (deferred execution). Enumerating again repeats the work; materialize when you need to retain the results. SingleOrDefault still throws for multiple matches. ToDictionary throws on duplicate keys. An IQueryable<T> provider may translate a query to SQL or another language, so its execution rules differ from in-memory LINQ.

Official guide — LINQ queries


Use yield return to produce an iterator’s elements one at a time without first storing them in an array.

foreach (int value in CountUp(3))
Console.WriteLine(value);
static IEnumerable<int> CountUp(int count)
{
for (int i = 0; i < count; i++)
yield return i;
}

Iterator bodies normally execute, and may throw, during enumeration rather than at the initial method call. yield break ends enumeration.


Catch specific exceptions you can handle. Use finally or using to release resources.

try
{
Console.WriteLine(ParsePositive("-1"));
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.ParamName);
}
catch (FormatException)
{
Console.WriteLine("Invalid number");
}
finally
{
Console.WriteLine("Finished");
}
static int ParsePositive(string text)
{
int value = int.Parse(text);
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(text), "Must be positive.");
return value;
}

Use throw; to rethrow while preserving the original stack trace. throw ex; resets it. For expected input failures, consider APIs such as TryParse instead of using exceptions for routine branching.


The GC manages memory, but resources such as files and connections need timely cleanup. using calls IDisposable.Dispose when its scope ends.

using var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, leaveOpen: true))
{
writer.Write("hello");
}
stream.Position = 0;
using var reader = new StreamReader(stream);
Console.WriteLine(reader.ReadToEnd());

Use await using for asynchronous cleanup through IAsyncDisposable. This differs from a using directive that imports a namespace.


Async methods commonly return Task or Task<T>. When an operation is incomplete, await can yield control to the caller. The async keyword does not itself start a new thread.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
Task<int> first = WorkAsync(1, cts.Token);
Task<int> second = WorkAsync(2, cts.Token);
int[] results = await Task.WhenAll(first, second);
Console.WriteLine(string.Join(", ", results));
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
Console.WriteLine("Canceled");
}
static async Task<int> WorkAsync(int value, CancellationToken cancellationToken)
{
await Task.Delay(20, cancellationToken);
return value * 2;
}

Cancellation is cooperative. Pass the token to downstream APIs and call ThrowIfCancellationRequested() in CPU work where appropriate. Task.WhenAll waits for every task; a failure does not automatically cancel the others.

Avoid blocking with .Result or .Wait() and propagate await through callers. Usually reserve async void for event handlers. Use asynchronous APIs for I/O; consider Task.Run when CPU-intensive work needs to run on another thread.

Official guide — asynchronous programming


IAsyncEnumerable<T> produces elements asynchronously. Enumerate it with await foreach.

using System.Runtime.CompilerServices;
using var cts = new CancellationTokenSource();
await foreach (int value in ReadAsync(cts.Token))
Console.WriteLine(value);
static async IAsyncEnumerable<int> ReadAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 3; i++)
{
await Task.Delay(10, cancellationToken);
yield return i;
}
}

Synchronize updates to shared state. Use Interlocked for simple counters and lock to protect a group of operations.

int count = 0;
Parallel.For(0, 1_000, _ => Interlocked.Increment(ref count));
Console.WriteLine(count);
var gate = new System.Threading.Lock();
List<int> values = [];
Parallel.For(0, 10, i =>
{
lock (gate)
{
values.Add(i);
}
});
Console.WriteLine(values.Count);

You cannot await inside a lock. For asynchronous concurrency limits, use SemaphoreSlim.WaitAsync and release in finally. Concurrent collections do not automatically make a sequence of multiple operations atomic.


Span<T> is a view over contiguous memory; slicing it does not copy elements. ReadOnlySpan<T> prevents writes through that view.

int[] values = [1, 2, 3, 4];
Span<int> middle = values.AsSpan(1, 2);
middle[0] = 20;
Console.WriteLine(values[1]);
ReadOnlySpan<char> prefix = "hello".AsSpan(0, 2);
Console.WriteLine(prefix.ToString());

Span<T> is a ref struct: it cannot be stored in an ordinary class field or used across an await boundary. Consider Memory<T> for those lifetimes. A readonly view does not prevent other code from modifying the underlying array.


Frequently used types and namespaces.

Purpose Type or namespace
Build strings System.Text.StringBuilder
Regular expressions System.Text.RegularExpressions.Regex
Dates and durations DateTimeOffset, DateOnly, TimeOnly, TimeSpan
Files and paths System.IO.File, Directory, Path
JSON System.Text.Json.JsonSerializer
HTTP System.Net.Http.HttpClient
UUIDs Guid
Non-cryptographic randomness Random.Shared
Cryptographic randomness System.Security.Cryptography.RandomNumberGenerator
Concurrent collections System.Collections.Concurrent
Async queues System.Threading.Channels
Elapsed time System.Diagnostics.Stopwatch

Use UTC or an explicit offset for stored or transmitted timestamps, then convert for display.

using System.Globalization;
DateTimeOffset now = DateTimeOffset.UtcNow;
string encoded = now.ToString("O", CultureInfo.InvariantCulture);
DateTimeOffset restored = DateTimeOffset.Parse(encoded, CultureInfo.InvariantCulture);
DateOnly date = new(2026, 9, 14);
TimeOnly time = new(9, 30);
TimeSpan duration = TimeSpan.FromMinutes(45);
Console.WriteLine($"{restored:O}, {date:yyyy-MM-dd}, {time:HH:mm}, {duration}");

DateTimeOffset stores an offset, not a time zone’s rules. Use TimeZoneInfo for conversions involving daylight saving time and other zone rules.

This example creates a temporary file, reads and writes it, then deletes it.

string path = Path.GetTempFileName();
try
{
await File.WriteAllTextAsync(path, "hello");
string text = await File.ReadAllTextAsync(path);
Console.WriteLine(text);
Console.WriteLine(Path.GetFileName(path));
}
finally
{
File.Delete(path);
}

Process large files incrementally with streams or File.ReadLines. Path.Combine joins paths; it does not validate that external input stays inside an allowed directory.

Use the built-in System.Text.Json APIs to serialize and deserialize data.

using System.Text.Json;
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var person = new Person("Ada", 36);
string json = JsonSerializer.Serialize(person, options);
Console.WriteLine(json);
Person restored = JsonSerializer.Deserialize<Person>(json, options)
?? throw new JsonException("Expected a person.");
Console.WriteLine(restored.Name);
public record Person(string Name, int Age);

Ordinary defaults match property names case-sensitively; web defaults differ. Validate business requirements after deserialization as well.

This example requires network access. Reuse HttpClient for an appropriate application lifetime, or manage it with IHttpClientFactory in a DI-based application.

using System.Net;
using var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
AutomaticDecompression = DecompressionMethods.All
};
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(10) };
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var response = await client.GetAsync("https://example.com/", cts.Token);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync(cts.Token));

This console example disposes the client on exit. Avoid creating and disposing a client per request in a long-running application. Handle HTTP failures, connection errors, timeouts, and cancellation according to your application’s requirements.

Official guide — HttpClient guidelines


Namespaces organize type names. The following is a file in a class library, not an executable Program.cs example.

namespace Sample.Library;
public static class Calculator
{
public static int Add(int left, int right) => left + right;
}

A file-scoped namespace applies to the whole file. using Sample.Library; shortens type names, but using another project also requires a project reference.

These commands create a separate solution for the examples. In .NET 10, dotnet new sln creates a .slnx file by default.

Terminal window
dotnet new sln -n CheatSheet
dotnet new classlib -n Sample.Library --framework net10.0
dotnet new console -n Sample.App --framework net10.0
dotnet sln CheatSheet.slnx add Sample.Library/Sample.Library.csproj Sample.App/Sample.App.csproj
dotnet reference add Sample.Library/Sample.Library.csproj --project Sample.App/Sample.App.csproj
dotnet restore CheatSheet.slnx
dotnet build CheatSheet.slnx
dotnet format CheatSheet.slnx --verify-no-changes
dotnet publish Sample.App/Sample.App.csproj -c Release

Add a NuGet package with dotnet package add PACKAGE --project PROJECT_PATH. Check its official NuGet page for stable releases and supported frameworks before adopting it. Use dotnet package list --outdated to check for updates. Pin package and SDK versions when reproducibility matters.


Add an MSTest project to the solution above. The template configures the test packages. This example explicitly selects MSTest 4.4.0.

Terminal window
dotnet new mstest -n Sample.Tests --framework net10.0
dotnet package add MSTest --version 4.4.0 --project Sample.Tests/Sample.Tests.csproj
dotnet sln CheatSheet.slnx add Sample.Tests/Sample.Tests.csproj
dotnet reference add Sample.Library/Sample.Library.csproj --project Sample.Tests/Sample.Tests.csproj
dotnet test CheatSheet.slnx

Place the Calculator shown above in Sample.Library, then replace the generated test file in the test project with this code.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sample.Library;
namespace Sample.Tests;
[TestClass]
public sealed class CalculatorTests
{
[TestMethod]
[DataRow(2, 3, 5)]
[DataRow(-2, 2, 0)]
[DataRow(0, 0, 0)]
public void Add_ReturnsSum(int left, int right, int expected)
{
Assert.AreEqual(expected, Calculator.Add(left, right));
}
}

This block belongs in the MSTest project and cannot run as a standalone console application. Run dotnet test CheatSheet.slnx again after the changes. Async tests should return async Task and await the operation.

Official guide — C# and MSTest


C# 14 adds extension members, field-backed properties, and null-conditional assignment, among other features. Here are representative examples.

Console.WriteLine("hello".IsLong);
var person = new Person();
person.Name = " Ada ";
Console.WriteLine(person.Name);
Person? missing = null;
missing?.Name = "Grace";
Console.WriteLine(nameof(List<>));
public static class TextExtensions
{
extension(string text)
{
public bool IsLong => text.Length >= 5;
}
}
public sealed class Person
{
public string Name
{
get;
set => field = value.Trim();
} = "";
}

Null-conditional assignment skips evaluation of the right-hand side when the receiver is null. field refers to a compiler-generated backing field. The existing this extension-method syntax remains supported.

Official reference — what’s new in C# 14


  • Losing fractions in integer division: convert at least one operand to a floating-point type or decimal before dividing.
  • Assuming == always means the same thing: ordinary classes default to reference equality, records use value equality, and strings compare content. Use SequenceEqual to compare array elements.
  • Relying on nullable !: it does not check for null at runtime. Validate input.
  • Enumerating LINQ repeatedly: watch for deferred execution and retain results with ToList or similar when needed.
  • Modifying a List<T> during foreach: use RemoveAll or project into another collection.
  • Forgetting to await: observe task completion and exceptions with await.
  • Treating async as parallelism: distinguish asynchronous I/O from parallel CPU work.
  • Generating secrets with Random: use RandomNumberGenerator for cryptographic purposes.
  • Storing DateTime.Now without time zone context: use UTC or an explicit offset.
  • Assuming records or readonly guarantee deep immutability: referenced objects can still be mutable.