Reflection allows you to access private members of a class. This is very useful when you want to access private members of a class that you don't own. However, it is slow and doesn't work well with Native AOT. In this post, I describe how to access private members without reflection starting from .NET 8.
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, you could access private members using reflection, or by generating IL at runtime. Both methods are slow. .NET 8 provides a new zero-overhead way to access private members. This is done using the [UnsafeAccessorAttribute] attribute. To access a private member, you can create an extern method with the [UnsafeAccessor] attribute to declare an accessor for a private member. Note that UnsafeAccessorAttribute is less powerful than reflection. Some methods or types cannot be accessed using this attribute, and you'll need to use reflection, Expression Trees, or dynamic code generation for 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 using a "container" type approach. In .NET 9, generic support is more complete, making it much easier to work with generic types.
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
One of the main limitations of [UnsafeAccessor] in .NET 8 and 9 is that you must be able to directly reference all types used in the method signature. .NET 10 introduces the [UnsafeAccessorType] attribute, which allows you to specify types as strings when you cannot reference them directly. This is particularly useful when working with internal or private types from other assemblies.
Instead of referencing a type directly, you can use object and add the [UnsafeAccessorType] attribute with the type name as a string.
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 in [UnsafeAccessorType] is a fully qualified type name, similar to what you would use with Type.GetType(). While assembly qualification is optional, it is recommended for robustness. For generic types, you must use the proper generic format (e.g., List``1[[!0]]), and nested classes require the + separator. If you are unsure about the exact type name, using typeof(YourType).AssemblyQualifiedName can help.
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. While you could declare the attribute yourself, it requires support from the runtime to understand it.
#Additional resources
Do you have a question or a suggestion about this post? Contact me!