A new way of doing reflection with .NET 8

19/09/2023

.NET 8 introduced a new way of doing reflection. Why did they introduce this, and what are some benefits - this blog post will give you some insights.

The old way

Before we go into detail of the new way, let's have a look how you would normally set a field via reflection:

public class Counter
{
    private readonly int _count = 0;
    public int CountPlusOne => _count + 1;
}

If we want to set _count, we would do the following:

var counter = new Counter();
typeof(Counter)
    .GetField("_count", BindingFlags.Instance | BindingFlags.NonPublic)!
    .SetValue(counter, 100);

The new way

The new way uses a very nice approach with a new attribute, called UnsafeAccessorAttribute.


var counter = new Counter();
GetCountField(counter) = 100;

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_count")]
private static extern ref int GetCountField(Counter counter);

The reason this was introduced is not only performance (which it definitely brings to the table) - no, but in recent versions, the .NET team pushes ahead of time (short AOT) compilation. Unfortunately, reflection doesn't always play well with AOT, especially with the tree-shaking part. Tree-shaking happens at compile time and basically removes everything that isn't needed. Of course, it can not look how you do reflection, so that can lead to unpleasant runtime issues. Think of System.Text.Json that can greatly benefit from that change.

Benchmark

Yes, that thing is also way faster:

[MemoryDiagnoser]
public class Benchmark
{
    private readonly Counter _counter = new();
    
    [Benchmark]
    public int DotNet7()
    {
        typeof(Counter)
            .GetField("_count", BindingFlags.Instance | BindingFlags.NonPublic)!
            .SetValue(_counter, 100);
        return _counter.CountPlusOne;
    }

    [Benchmark]
    public int DotNet8()
    {
        GetCountField(_counter) = 100;
        return _counter.CountPlusOne;
    }
    
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_count")]
    private static extern ref int GetCountField(Counter counter);
}

public class Counter
{
    private readonly int _count = 0;
    public int CountPlusOne => _count + 1;
}

Results:

BenchmarkDotNet v0.13.8, macOS Ventura 13.5.2 (22G91) [Darwin 22.6.0]
Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores
.NET SDK 8.0.100-rc.1.23455.8
  [Host]     : .NET 8.0.0 (8.0.23.41904), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 8.0.0 (8.0.23.41904), Arm64 RyuJIT AdvSIMD


| Method  | Mean       | Error     | StdDev    | Gen0   | Allocated |
|-------- |-----------:|----------:|----------:|-------:|----------:|
| DotNet7 | 32.6988 ns | 0.5648 ns | 0.5283 ns | 0.0029 |      24 B |
| DotNet8 |  0.0725 ns | 0.0058 ns | 0.0051 ns |      - |         - |

What's next

Currently, there are some limitations to the new approach - the team wants to introduce a generic version of the attribute, as well as enable private and static types. I will link those issues down in the resource section if you want to read further.

Resources

7
An error has occurred. This application may no longer respond until reloaded. Reload x