NDepend Blog

Improve your .NET code quality with NDepend

Modern .NET Reflection with UnsafeAccessor

November 10, 2025 4 minutes read

Modern .NET Reflection with UnsafeAccessor

Reflection in .NET has traditionally been powerful but slow. Starting with .NET 8, the [UnsafeAccessor] attribute provides a zero-overhead alternative for accessing private members, offering performance comparable to direct access and a strongly typed syntax at call site.

Traditional Reflection

Before UnsafeAccessor, accessing private fields or methods required this reflection syntax:

This approach suffers from:

  • Runtime overhead from reflection calls
  • Boxing/unboxing for value types
  • Difficult to optimize for the JIT compiler
  • Multiple API calls: GetField(), GetValue()…

.NET 8: UnsafeAccessor Introduction

.NET 8 introduced System.Runtime.CompilerServices.UnsafeAccessor to solve these problems. It works by declaring extern methods that the runtime implements with direct access:

Remarks:

  • GetSSN() returns a ref string, meaning it doesn’t return a reference to the string but a managed pointer to the actual field inside the employee object. Through this managed pointer, you can both read the current value and directly assign a new string to the underlying field.
  • If the member specified by the Name parameter doesn’t exist, the runtime throws a MissingFieldException or MissingMethodException. The extern method’s signature is strongly typed, ensuring compile-time safety at the call site. However, like with traditional reflection, there are no compile-time guarantees that the target specified by the string actually exists.
  • The enum System.Runtime.CompilerServices.UnsafeAccessorKind is defined as follow:

  • Finally, unsafe accessor methods can be declared within the current type and also as a local method within a method. Below is the code rewritten with these methods declared in the Program class generated by the top-level statements syntax:

.NET 9: Enhanced Generic Support

.NET 9 improved UnsafeAccessor with better generic type handling and more flexible member access:

Unlike in the previous section, here the generic class CacheAccessor<T> is required to declare the type parameter T. This is because the unsafe accessor methods cannot be declared as generic themselves. Doing so compiles since it is valid C#, but it fails at runtime:

Accessing private members of generic class is useful, for instance, to access the internals of generic collections. This can also be achieved also using System.Runtime.InteropServices.CollectionsMarshal. Both ways are illustrated by the program below:

Naturally, the array backing a List<T> is private for a reason. When elements are added or removed, the list implementation may reallocate a larger or smaller array. Directly manipulating this private array can therefore cause subtle and complex bugs if the list’s size changes concurrently.

Benchmarking UnsafeAccessor Reflection vs Traditional reflection

The benchmark below extracted from the article Performance Improvements in .NET 10 by Stephen Toub, proves that UnsafeAccessor based reflection is faster than traditional reflection:

Here is the result:

.NET 10.0 UnsafeAccessorType

Before .NET 10.0, if a parameter or return type could not be resolved at compile time, UnsafeAccessor-based reflection would fail at runtime. This typically occurs when encapsulation restricts type visibility, such as with a private nested type or an internal type not visible to external assemblies.

.NET 10.0 addresses this limitation with the UnsafeAccessorType attribute. The fully-qualified name of the hidden type name must be provided as a string. The following program demonstrates how it works with the private nested type HiddenClass.

Commenting UnsafeAccessorType usage leads to an exception thrown at runtime:

unsafe-accessor-hidden-class-failure

Conclusion

Bypassing visibility isn’t something you do every day, but it’s convenient to have a concise, zero-overhead way to do it when needed.

Accessing private members via reflection is generally discouraged, but it can be justified in certain situations. For example when accessing dictionary internal layout with managed pointers to increase performance.

In the blog post already referenced Performance Improvements in .NET 10, Stephen Toub explains another scenario:

“An example of where this is handy is with a cyclic relationship between System.Net.Http and System.Security.CryptographySystem.Net.Http sits above System.Security.Cryptography, referencing it for critical features like X509Certificate. But System.Security.Cryptography needs to be able to make HTTP requests in order to download OCSP information, and with System.Net.Http referencing System.Security.CryptographySystem.Security.Cryptography can’t in turn explicitly reference System.Net.Http. It can, however, use reflection or [UnsafeAccessor] and [UnsafeAccessorType] to do so, and it does. It used to use reflection, now in .NET 10 it uses [UnsafeAccessor].”

 

 

This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!