Reflection allows you to access private members of a class you don't own. However, it is slow and doesn't work well with Native AOT. Starting with .NET 8, there is a better way.
Let's consider the following class containing private members:
C#
class Sample
{
// Constructors
private Sample() { }
private Sample(int value) { }
// Fields
private int instanceField = 1;
private readonly int instanceFieldRO = 2;
// Static fields
private static int staticField = 3;
private static readonly int staticFieldRO = 4;
// Properties
public int InstanceProperty { get; set; }
public int StaticProperty { get; set; }
// Methods
private int InstanceMethod(int value) => value;
private static int StaticMethod(int value) => value;
}
Before .NET 8, accessing private members required reflection or runtime IL generation, both of which are slow. .NET 8 introduces a zero-overhead alternative via the [UnsafeAccessor] attribute. To access a private member, declare an extern method decorated with this attribute. Note that [UnsafeAccessor] is less powerful than reflection: some members or types are not accessible this way, so reflection, Expression Trees, or dynamic code generation may still be needed in those cases.
#Constructors
C#
// Call private constructors
var sample1 = CallPrivateConstructorClass();
var sample2 = CallPrivateConstructorClassWithArg(1);
// The return type is the type of the class containing the private constructor.
// The argument must match the argument of the private constructor.
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
extern static Sample CallPrivateConstructorClass();
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
extern static Sample CallPrivateConstructorClassWithArg(int value);
#Instance methods
C#
var sample = CallPrivateConstructorClass();
Console.WriteLine(InstanceMethod(sample, 1));
// The first argument is the instance of the class containing the private method.
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "InstanceMethod")]
extern static int InstanceMethod(Sample @this, int value);
#Static methods
C#
Console.WriteLine(StaticMethod(null, 2));
// The first argument must be of the type containting the private method.
// Even if a static method doesn't use an instance, the runtime needs to know
// the type of the class.
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "StaticMethod")]
extern static int StaticMethod(Sample @this, int value);
#Instance properties
Properties are accessible through the getter and setter methods (get_{PropertyName}, set_{PropertyName}).
C#
var sample = CallPrivateConstructorClass();
InstanceSetter(sample, 42);
Console.WriteLine(InstanceGetter(sample));
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_InstanceProperty")]
extern static void InstanceSetter(Sample @this, int value);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InstanceProperty")]
extern static int InstanceGetter(Sample @this);
#Static properties
C#
StaticSetter(null, 42);
Console.WriteLine(StaticGetter(null));
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "set_StaticProperty")]
extern static void StaticSetter(Sample @this, int value);
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_StaticProperty")]
extern static int StaticGetter(Sample @this);
#Instance fields
C#
// Use "ref" to get a reference to the field, so you can read and write to it.
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "instanceField")]
extern static ref int GetInstanceField(Sample @this);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "instanceFieldRO")]
extern static ref int GetInstanceReadOnlyField(Sample @this);
var sample = CallPrivateConstructorClass();
// Read field value
_ = GetInstanceField(sample);
// Write field value
GetInstanceField(sample) = 42;
// Even if a field is readonly, this is just a compiler check.
// So, you can write the value of a instance readonly field (same as when using reflection).
GetInstanceReadOnlyField(sample) = 42;
If the type is a struct, the first parameter must be ref:
C#
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_a")]
extern static ref int GetInstanceField(ref Guid @this);
#Static fields
C#
// Even if a static field doesn't use an instance, the runtime needs to know the type
// containing the private field. So, the "@this" argument is required.
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "staticField")]
extern static ref int GetStaticField(Sample @this);
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "staticFieldRO")]
extern static ref int GetStaticReadOnlyField(Sample @this);
var sample = CallPrivateConstructorClass();
// Read the value
_ = GetStaticField(sample);
// Write the value
GetStaticField(sample) = 42;
// ⚠️ You can write the value of a static readonly field, but be careful if you do so.
// static readonly fields are very similar to constant for the JIT. So, setting the value
// can lead to unexpected behavior as the value may not be read again.
// Note that this is not possible using reflection.
// (Remember that the attribute name starts with "Unsafe", so it could allow thing that are not safe).
GetStaticReadOnlyField(sample) = 42; // ok
// This fails at runtime
typeof(Sample).GetField("staticFieldRO", BindingFlags.Static | BindingFlags.NonPublic)
.SetValue(null, 44);
#Generic support in .NET 9
.NET 9 added comprehensive support for generic types and methods with [UnsafeAccessor]. Prior to .NET 9, accessing private members of generic types required a workaround using a wrapper type. The improved support makes working with generic types significantly easier.
C#
// Accessing a private field on a generic type
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
static extern ref T[] GetItemsField<T>(List<T> list);
// Using the accessor
var list = new List<int> { 1, 2, 3 };
ref var items = ref GetItemsField(list);
Console.WriteLine(items.Length);
#Accessing unreferenced types with [UnsafeAccessorType] in .NET 10
A key limitation of [UnsafeAccessor] in .NET 8 and 9 is that all types used in the method signature must be directly referenceable. .NET 10 introduces the [UnsafeAccessorType] attribute, which lets you specify types by name as strings. This is particularly useful when working with internal or private types from other assemblies.
With this approach, use object as the parameter or return type and annotate it with [UnsafeAccessorType], providing the fully qualified type name.
Here's an example accessing a private type from another assembly:
C#
// Accessing an internal type's field
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_item")]
static extern string GetPrivateField([UnsafeAccessorType("MyNamespace.MyFullTypeName, MyAssemblyName")] object instance);
// Accessing a method on an internal type
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "MethodName")]
[return: UnsafeAccessorType("MyNamespace.MyReturnType, MyAssemblyName")]
static extern object InvokeMethod([UnsafeAccessorType("MyNamespace.MyFullTypeName, MyAssemblyName")] object instance);
// Using the accessors (you would need to create the instance using reflection or another UnsafeAccessor)
// object instance = ...; // Create instance of InternalClass
object fieldValue = GetPrivateField(instance);
object methodResult = InvokeMethod(instance);
##Type name specification
The type name used in [UnsafeAccessorType] follows the same format as Type.GetType(). Assembly qualification is optional but recommended for robustness. Generic types require the proper generic format (e.g., List``1[[!0]]), and nested classes use the + separator. When unsure of the exact type name, typeof(YourType).AssemblyQualifiedName is a helpful reference.
Here are some examples showing different type name patterns:
C#
// Simple type with assembly qualification
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
[return: UnsafeAccessorType("MyNamespace.MyClass, MyAssembly")]
extern static object CreateClass();
// Array type
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetArray")]
[return: UnsafeAccessorType("MyNamespace.MyClass[], MyAssembly")]
extern static object CallGetArray([UnsafeAccessorType("MyNamespace.MyClass, MyAssembly")] object instance);
// Closed generic type (List<Class1>)
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ClosedGeneric")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[MyNamespace.MyClass, MyAssembly]]")]
extern static object CallGeneric([UnsafeAccessorType("MyNamespace.MyClass`1[[!0]], MyAssembly")] object instance);
// Open generic method (!0 represents the type parameter from the declaring type, !!0 represents the method's type parameter)
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericMethod")]
[return: UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")]
extern static object CallGenericMethod<U>([UnsafeAccessorType("MyNamespace.MyClass`1[[!0]], MyAssembly")] object instance);
// By-reference parameter
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "MethodWithRef")]
extern static void CallMethodWithRef(
[UnsafeAccessorType("PrivateLib.Class1, MyAssembly")] object instance,
[UnsafeAccessorType("PrivateLib.Class1&, MyAssembly")] ref object parameter);
#Can I use this feature in previous versions of .NET?
The [UnsafeAccessor] feature cannot be polyfilled. Even if you declare the attribute yourself, it requires native runtime support to function.
#Additional resources
Do you have a question or a suggestion about this post? Contact me!