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.