DEV Community

Cover image for Reflection via source generators
Stanislav Silin
Stanislav Silin

Posted on

Reflection via source generators

Lately, I have been experimenting with source generation and got an idea. What if I build a reflection via source generation. How will it differ from the default one? And I did it.

So, this library aims to create a subset of reflection that will be faster than the default one and will not break at the platforms with the AOT compilation support. The source generators will help us with that.

How to use

To make it work, you will need to install a NuGet package Apparatus.AOT.Reflection:

dotnet add package Apparatus.AOT.Reflection
Enter fullscreen mode Exit fullscreen mode

Then you can use it like that:

public class User
{
    [Required]
    public string FirstName { get; set; }
    [Required]
    public string LastName { get; set; }
}

public static void Main()
{
    var user = new User();
    var properties = user.GetProperties().Values;
    foreach (var property in properties)
    {
        Console.WriteLine(property.Name);
    }
}
Enter fullscreen mode Exit fullscreen mode

This sample will print the names of properties.

FirstName
LastName
Enter fullscreen mode Exit fullscreen mode

Also, it works for enums too:


public enum UserKind 
{
    User,
    Admin
}

// ...

public static void Main()
{
    var values = EnumHelper.GetEnumInfo<UserKind>();
    foreach (var value in values)
    {
        Console.WriteLine(value.Name);
    }
}

Enter fullscreen mode Exit fullscreen mode

You will see:

User
Admin
Enter fullscreen mode Exit fullscreen mode

It does not end with the only property names. You can get property values and assigned attributes.

Here is an example:

var requiredProperties = _user
    .GetProperties()
    .Values
    .Where(o => o.Attributes.Any(attr => attr is RequiredAttribute))
    .ToArray();

foreach (var requiredProperty in requiredProperties)
{
    if (requiredProperty.TryGetValue(_user, out var value))
    {
        Console.WriteLine($"{requiredProperty.Name} => {value}");
    }
}
Enter fullscreen mode Exit fullscreen mode

The same applies to enums too. Let have a look at the following sample:

public enum AccountKind
{
    [Description("User account")]
    User,
    [Description("Admin account")]
    Admin,
    [Description("Customer account")]
    Customer,
    [Description("Manager account")]
    Manager
}

// ...

var values = EnumHelper.GetEnumInfo<AccountKind>();
foreach (var value in values)
{
    var description = value.Attributes
        .OfType<DescriptionAttribute>()
        .First();

    Console.WriteLine($"{value.Name} => {description.Description}");
}
Enter fullscreen mode Exit fullscreen mode

Performance

Let's imagine that we need to find a property with Required attribute and the name FirstName.
If it exists, then print the value of the property, otherwise return the empty string. The implementation will be messy because I don't want to measure the LINQ performance, but the overall idea must be clear.

Here is the source code with default reflection:

var type = _user.GetType();
var property = type.GetProperty(nameof(User.FirstName));

var required = false;
foreach (var o in property.GetCustomAttributes())
{
    if (o.GetType() == typeof(RequiredAttribute))
    {
        required = true;
        break;
    }
}

if (required)
{
    return (string)property.GetMethod?.Invoke(_user, null);
}

return string.Empty;

Enter fullscreen mode Exit fullscreen mode

Here the source code with aot reflection:

var entries = _user.GetProperties();
var firstName = entries[nameof(User.FirstName)];

var required = false;
foreach (var o in firstName.Attributes)
{
    if (o is RequiredAttribute)
    {
        required = true;
        break;
    }
}

if (required)
{
    if (firstName.TryGetValue(_user, out var value))
    {
        return (string)value;
    }

    return string.Empty;
}

return string.Empty;
Enter fullscreen mode Exit fullscreen mode

Here are the benchmark results:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1165 (21H1/May2021Update)
11th Gen Intel Core i7-11700KF 3.60GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.100-preview.7.21379.14
  [Host]     : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
  DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT


|        Method |        Mean |    Error |   StdDev |  Gen 0 | Allocated |
|-------------- |------------:|---------:|---------:|-------:|----------:|
|    Reflection | 1,758.91 ns | 2.714 ns | 2.406 ns | 0.1278 |   1,072 B |
| AOTReflection |    16.01 ns | 0.090 ns | 0.075 ns |      - |         - |
Enter fullscreen mode Exit fullscreen mode

As you can see, the AOT.Reflection is significantly faster comparing to default reflection.

Now let's have a look at enums performance. Imagine that we have the enum value, and we need to get a description associated with it.
Here how it will look:

var attributes = _account.GetEnumValueInfo().Attributes;
for (int i = 0; i < attributes.Length; i++)
{
    var attribute = attributes[i];
    if (attribute is DescriptionAttribute descriptionAttribute)
    {
        return descriptionAttribute.Description;
    }
}

return "";
Enter fullscreen mode Exit fullscreen mode

Here is the results:

|              Method |       Mean |     Error |    StdDev |  Gen 0 | Allocated |
|-------------------- |-----------:|----------:|----------:|-------:|----------:|
|        GetValuesAOT |   6.253 ns | 0.0394 ns | 0.0329 ns |      - |         - |
| GetValuesReflection | 734.563 ns | 2.3173 ns | 1.9351 ns | 0.0324 |     272 B |
Enter fullscreen mode Exit fullscreen mode

And again, the AOT reflection works much faster.

The complete source code of benchmarks you can find here.

Limitations

I would recommend being careful when you try to use these APIs inside the generic methods because, at this point, there is no easy way to analyze them and identify the correct signatures. It means the source generation will not happen. As a result, we will have an error at runtime.
Let's have a look at the following sample:

public class Program
{
    public static string? GetDescription<T>(T enumValue)
        where T : Enum
    {
        return enumValue
            .GetEnumValueInfo()
            .Attributes
            .OfType<DescriptionAttribute>()
            .FirstOrDefault()
            ?.Description;
    }

    public static void Main()
    {
        var account = AccountKind.Admin;
        Console.WriteLine(GetDescription(account));
    }
}
Enter fullscreen mode Exit fullscreen mode

We will have an exception if we run it because the source generator could not figure out the signatures. The type T is the mystery for it.
But we can fix it with a small trick:

public class Program
{
    private void DontCallMe()
    {
        EnumHelper.GetEnumInfo<AccountKind>();
    }

    public static string? GetDescription<T>(T enumValue)
        where T : Enum
    {
        return enumValue
            .GetEnumValueInfo()
            .Attributes
            .OfType<DescriptionAttribute>()
            .FirstOrDefault()
            ?.Description;
    }

    public static void Main()
    {
        var account = AccountKind.Admin;
        Console.WriteLine(GetDescription(account));
    }
}

Enter fullscreen mode Exit fullscreen mode

Pay attention to the DontCallMe method. We do not have any intention to use it anywhere. It is here to help the source generator to analyze the source code. Now, if we run it, everything works as expected.
The same issue exists for the properties reflection, and we can use the same trick to avoid it.

Support

Right now, only public properties and enums are supported. Regarding the private members, I doubt them because they would ruin the performance, but we will see.

The end

Thank you for your time!

Links: Github, Nuget

Oldest comments (3)

Collapse
 
ergisgjergji profile image
ergisgjergji • Edited

Question: can you explain the reason as to why is it faster? Doesn't your library use Reflection internally?

Collapse
 
timur_kh profile image
Timur Kh • Edited

I believe full-fat reflection is unavailable when source code generators kick in (and this is why generics are such a pain). From a quick glance over the code, the library appears to analyse class structure and generate metadata at compile time.
Then it uses .GetProperties extension to fetch it as if it was reflected upon. But in fact all it does is a dictionary lookup. Hence speed

Collapse
 
ergisgjergji profile image
ergisgjergji

Ah, all right then. Good job and thanks for the sharing :)