Skip to main content

Newsletter Archive

Browse through our collection of past newsletters. Each edition is packed with C# and .NET insights.

Page 3 of 19 (56 editions)

August 30, 2026

You’re writing a unit test and you need to verify some internal state. The field is private. Your options used to be:

  1. [InternalsVisibleTo] and make it internal (pollutes your public API design)
  2. Reflection with BindingFlags.NonPublic (slow, breaks under trimming/AOT, string-based)
  3. Add a test-only public method (yuck)

.NET 8 introduced [UnsafeAccessor], which gives you a direct, zero-overhead, AOT-compatible way to access private fields and methods through a compiler-generated extern method.

Accessing a Private Field

using System.Runtime.CompilerServices;
// The class under test — you don't control this
public class ConnectionPool
{
private readonly List<Connection> _connections = [];
private int _activeCount;
public void Acquire() { /* ... */ _activeCount++; }
public void Release() { /* ... */ _activeCount--; }
}
// In your test project — zero-cost accessor
public partial class ConnectionPoolTests
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_activeCount")]
private static extern ref int GetActiveCount(ConnectionPool pool);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_connections")]
private static extern ref List<Connection> GetConnections(ConnectionPool pool);
[Fact]
public void Acquire_IncrementsActiveCount()
{
var pool = new ConnectionPool();
pool.Acquire();
pool.Acquire();
Assert.Equal(2, GetActiveCount(pool));
}
[Fact]
public void Release_DecrementsActiveCount()
{
var pool = new ConnectionPool();
pool.Acquire();
pool.Release();
Assert.Equal(0, GetActiveCount(pool));
}
}

No reflection. No BindingFlags. No string-based property lookup that breaks when you rename the field. The JIT resolves the access at compile time and inlines it — it’s literally as fast as accessing the field directly.

Calling a Private Method

public class EmailService
{
private string SanitizeAddress(string email)
=> email.Trim().ToLowerInvariant();
}
// Test accessor
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SanitizeAddress")]
private static extern string CallSanitize(EmailService service, string email);
[Fact]
public void SanitizeAddress_TrimsAndLowercases()
{
var service = new EmailService();
var result = CallSanitize(service, " [email protected] ");
Assert.Equal("[email protected]", result);
}

Accessing a Private Static Field

public class RateLimiter
{
private static int _globalRequestCount;
}
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "_globalRequestCount")]
private static extern ref int GetGlobalCount(RateLimiter? _);
[Fact]
public void Test_GlobalCount()
{
// Pass null for static accessors — the instance isn't used
ref int count = ref GetGlobalCount(null);
Assert.Equal(0, count);
}

Why “Unsafe”?

The name is honest: you’re bypassing encapsulation. The runtime won’t stop you from reading or writing private state. This is explicitly a testing and tooling escape hatch, not something for production business logic. Use it the way you’d use reflection — to verify internal state in tests, to build diagnostic tools, or to interop with legacy code you can’t modify.

Why Not Just Use Reflection?

ReflectionUnsafeAccessor
SpeedSlow (method lookup + invoke)Zero overhead (JIT-inlined)
AOT/Trimming❌ Breaks✅ Works
Compile-time safety❌ Strings✅ Signature-checked
AllocationsYes (boxing, object[])None

Key Takeaway

[UnsafeAccessor] gives you direct, zero-cost, trim-safe access to private fields and methods. Use it in tests to verify internal state without compromising your public API design. It’s reflection’s speed and AOT limitations — solved.

Read the full tip + 6 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.

August 23, 2026

Quick: how do you create a List<int> with three items? new List<int> { 1, 2, 3 }? Or maybe new List<int>([1, 2, 3])? An array? new int[] { 1, 2, 3 }. A span? stackalloc int[] { 1, 2, 3 }. An immutable array? ImmutableArray.Create(1, 2, 3).

Every collection type had its own ceremony. C# 12 introduces collection expressions — one syntax that targets them all:

// All of these use the same [1, 2, 3] syntax
int[] array = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
ReadOnlySpan<int> roSpan = [1, 2, 3];
ImmutableArray<int> immutable = [1, 2, 3];
HashSet<int> set = [1, 2, 3];

The compiler figures out what to emit based on the target type. For Span<int>, it stack-allocates. For List<int>, it creates a list with the right capacity. For ImmutableArray<int>, it uses the builder. You don’t need to know the optimal construction API for each type. The compiler does.

The Spread Operator

Need to combine collections? The .. spread operator inlines one collection into another:

int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] combined = [..first, ..second]; // [1, 2, 3, 4, 5, 6]
int[] withExtra = [0, ..first, ..second, 7, 8]; // [0, 1, 2, 3, 4, 5, 6, 7, 8]

This replaces the Concat + ToArray pattern, and the compiler can optimize it into a single allocation with known size.

Practical Example: Building Middleware Pipelines

public static IEnumerable<string> GetCorsOrigins(bool isDevelopment)
{
string[] production = ["https://app.example.com", "https://admin.example.com"];
string[] development = ["http://localhost:3000", "http://localhost:5173"];
return isDevelopment
? [..production, ..development]
: production;
}

No Enumerable.Concat(). No .ToArray(). No LINQ allocation chain. Just a clean expression.

Empty Collections

The empty collection literal [] is also the new best way to return or initialize an empty collection:

// Before: Array.Empty<string>(), new List<string>(), Enumerable.Empty<string>()
// After:
string[] empty = [];
List<string> emptyList = [];
ImmutableArray<string> emptyImmutable = [];

The compiler emits the most efficient empty representation for each target type (Array.Empty<T>() for arrays, a cached empty instance for immutable arrays, etc.).

Key Takeaway

[1, 2, 3] works for arrays, lists, spans, immutable collections, and sets. The .. spread operator combines them without LINQ overhead. One syntax, all collection types, zero ceremony. If you’re still writing new List<int> { ... }, you’re writing more characters than you need to.

Read the full tip + 6 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.

August 16, 2026

You’ve got five API calls firing in parallel, and you want to process each result as soon as it lands instead of waiting for the slowest one to finish. Before .NET 9, this meant a gnarly while loop with Task.WhenAny, removing completed tasks from a list, and praying you didn’t mess up the bookkeeping. Task.WhenEach makes that entire pattern a one-liner.

Here’s the old way (functional, but ugly):

// The "before": nobody enjoyed writing this
var tasks = new List<Task<string>>
{
FetchAsync("https://api.example.com/users"),
FetchAsync("https://api.example.com/orders"),
FetchAsync("https://api.example.com/products"),
};
while (tasks.Count > 0)
{
var completed = await Task.WhenAny(tasks);
tasks.Remove(completed);
Console.WriteLine(await completed);
}

And here’s the .NET 9 way:

using System.Net.Http;
var client = new HttpClient();
var tasks = new[]
{
FetchAsync("https://jsonplaceholder.typicode.com/posts/1"),
FetchAsync("https://jsonplaceholder.typicode.com/posts/2"),
FetchAsync("https://jsonplaceholder.typicode.com/posts/3"),
};
await foreach (var completed in Task.WhenEach(tasks))
{
var result = await completed; // already done, returns instantly
Console.WriteLine($"Got: {result[..Math.Min(80, result.Length)]}...");
}
async Task<string> FetchAsync(string url)
{
await Task.Delay(Random.Shared.Next(100, 1000)); // simulate variable latency
return await client.GetStringAsync(url);
}

Task.WhenEach returns an IAsyncEnumerable<Task<T>> that yields each task in the order it completes, not the order it was started. You just await foreach over it and handle results as they stream in. The code reads like English, and there’s zero manual bookkeeping.

This pattern is perfect for fan-out scenarios: calling multiple microservices, scraping multiple URLs, or running parallel database queries. Anywhere the response times vary and you want to start processing ASAP, this delivers. Combine it with a CancellationToken and you can even bail out early once you’ve got what you need.

No more WhenAny loops. No more mutable lists. Just await foreach and vibes.

Read the full tip + 6 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.