Accessing private members without reflection in C#

 
 
  • Gérald Barré

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 in .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 powerfull than reflection. For instance, generic types are not fully supported yet (dotnet/runtime#89439).

#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;

#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);

#Can I use this feature in previous versions of .NET?

No, this feature cannot be polyfilled. While you could declare the attribute yourself, it requires support from the runtime to understand it. So only .NET 8 and later versions support it.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub