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.