A new way of doing reflection with .NET 8

9/19/2023
3 minute read

.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

A more flexible and enhanced way of logging in .NET 8

The latest version of the .NET (version 8) has introduced a "better" way of logging. This new way of logging is more flexible and enhanced than the previous versions. It is about the LoggerMessageAttribute.

.NET 8 and Blazor United / Server-side rendering

New .NET and new Blazor features. In this blog post, I want to highlight the new features that are hitting us with .NET 8 in the Blazor world. So let's see what's new.

New ArgumentException and ArgumentOutOfRangeException helpers in .NET 8

Do you remember how .NET 6 introduced the ArgumentNullException.ThrowIfNull guard? And afterward, with .NET 7 we've got this excellent bit: ArgumentException.ThrowIfNullOrEmpty? Guess what, there might come some new handy additions for the upcoming .NET 8 iteration.

So let's see what are those new changes and how they make the code simpler.

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