Newsletter Archive
Browse through our collection of past newsletters. Each edition is packed with C# and .NET insights.
Page 3 of 19 (56 editions)
You’re writing a unit test and you need to verify some internal state. The field is private. Your options used to be:
[InternalsVisibleTo]and make itinternal(pollutes your public API design)- Reflection with
BindingFlags.NonPublic(slow, breaks under trimming/AOT, string-based) - 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 thispublic class ConnectionPool{ private readonly List<Connection> _connections = []; private int _activeCount;
public void Acquire() { /* ... */ _activeCount++; } public void Release() { /* ... */ _activeCount--; }}
// In your test project — zero-cost accessorpublic 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();
}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?
| Reflection | UnsafeAccessor | |
|---|---|---|
| Speed | Slow (method lookup + invoke) | Zero overhead (JIT-inlined) |
| AOT/Trimming | ❌ Breaks | ✅ Works |
| Compile-time safety | ❌ Strings | ✅ Signature-checked |
| Allocations | Yes (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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
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] syntaxint[] 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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
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 thisvar 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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.