AOTプロジェクトでリフレクションを楽しく使う

リフレクションは.NET開発の強力なツールですが、AOTではネイティブコンパイルのため、リフレクションの機能は基本的にAOTコンパイルのプロジェクトで失効します。しかし、困難よりも解決策があります。このような素晴らしいツールを捨てるわけにはいきません。以下は、リフレクションを「可能な限り」使用する例です。「可能な限り」という言葉の意味については、以下のケースを見てから説明します。

AOTプロジェクトでリフレクションを使用する基本原理：ビルドプロジェクト時にソースジェネレータを利用し、反映させたい各タイプのGetMemberを事前に呼び出します。

1. まず、AOTReflectionHelperプロジェクトを作成します。このプロジェクトにはAOTReflectionAttributeというクラスが1つだけ含まれており、このクラスは部分クラスです。
```csharp
using System;

namespace AOTReflectionHelper
{
    [AttributeUsage(AttributeTargets.Class)]
    public partial class AOTReflectionAttribute : Attribute
    {
    }
}
```
2. 次に、ソースジェネレータのプロジェクトであるAOTReflectionGeneratorプロジェクトを作成します。プロジェクトファイルは以下の通りです：
```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>12.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.23525.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.0-2.final" />
  </ItemGroup>
</Project>
```
ソースジェネレータの実装コードは以下の通りです。基本的なアイデアは、上述のAOTReflectionAttributeクラスの部分クラスを生成し、コンストラクタ内でこの属性を持つすべてのタイプのGetMembersメソッドを呼び出すことです。ビルド時にリフレクションを使用してメンバーを取得するので、この部分に問題はありません。

```csharp

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Text;

namespace AOTReflectionGenerator
{
    [Generator]
    public class AOTReflectionGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            var types = GetAOTReflectionAttributeTypeDeclarations(context);
            var source = BuildSourse(types);
            context.AddSource($"AOTReflectionGenerator.g.cs", source);
        }
        string BuildSourse(IEnumerable<(string NamespaceName, string ClassName)> types)
        {
            var codes = new StringBuilder();
            foreach (var type in types)
            {
                codes.AppendLine($"   typeof({(type.NamespaceName != "<global namespace>" ? type.NamespaceName + "." : "")}{type.ClassName}).GetMembers();");
            }
            var source = $$"""
                         using System;
                         [AttributeUsage(AttributeTargets.Class)]
                         public partial class AOTReflectionAttribute:Attribute
                         {
                            public AOTReflectionAttribute()
                            {
                            {{codes}}
                            }
                         }
                         """;         
            return source;
        }
        IEnumerable<(string NamespaceName, string ClassName)> GetAOTReflectionAttributeTypeDeclarations(GeneratorExecutionContext context)
        {
            var list = new List<(string, string)>();
            foreach (var tree in context.Compilation.SyntaxTrees)
            {
                var semanticModel = context.Compilation.GetSemanticModel(tree);
                var root = tree.GetRoot(context.CancellationToken);
                var typeDecls = root.DescendantNodes().OfType<TypeDeclarationSyntax>();
                foreach (var decl in typeDecls)
                {                  
                    var symbol = semanticModel.GetDeclaredSymbol(decl);                    
                    if (symbol?.GetAttributes().Any(attr => attr.AttributeClass?.Name == "AOTReflectionAttribute") == true)
                    {                      
                        var className = decl.Identifier.ValueText;
                        var namespaceName = symbol.ContainingNamespace?.ToDisplayString();
                        list.Add((namespaceName, className));
                    }
                }
            }
            return list;
        }

        public void Initialize(GeneratorInitializationContext context)
        {
        }
    }
}
```
3. APITestプロジェクトはテストプロジェクトです。プロジェクトファイルは以下の通りです。12行目のソースジェネレータには2つの属性が必要です。
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\AOTReflectionGenerator\AOTReflectionGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
    <ProjectReference Include="..\AOTReflectionHelper\AOTReflectionHelper.csproj" />
  </ItemGroup>
</Project>
```
実装コードは、リフレクションを使用したいタイプに[AOTReflection]を追加するだけで、GetPropertiesメソッドを25行目のメソッドで使用できるようになります。そうでなければ、このメソッドは空の配列を返します。

```csharp
using System.Text.Json.Serialization;
using System.Text;
using APITest.Models;
using AOTReflectionHelper;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();
app.MapGet("/test", () =>
{
    var order = new Order { Name = "桂素伟", Age = 10, Birthday = DateTime.Now, Hobbies = new string[] { "足球", "代码" } };
    InvockMethod(order);
    return GetString(order);

});
app.MapPost("/test", (Person person) =>
{
    return GetString(person);
});
string GetString<T>(T t) where T : Parent
{
    var sb = new StringBuilder();
    var pros = typeof(T)?.GetProperties();
    foreach (var pro in pros)
    {      
        if (pro != null)
        {
            if (pro.PropertyType.IsArray)
            {
                var arr = pro.GetValue(t) as string[];
                sb.Append($"{pro?.Name}:{string.Join(",", arr)};");
            }
            else
            {
                sb.Append($"{pro?.Name}:{pro?.GetValue(t)};");
            }
        }
    }
    t.Print(sb.ToString());
    return sb.ToString();
}
void InvockMethod<T>(T t)
{
    var method = typeof(T)?.GetMethod("Print");
    method?.Invoke(t, new object[] { "用反射调用Print" });
}
app.Run();

[JsonSerializable(typeof(Person))]
[JsonSerializable(typeof(string[]))]
public partial class AppJsonSerializerContext : JsonSerializerContext
{
}
public partial class Parent
{
    public void Print(string content)
    {      
        Console.WriteLine($"反射类型名：{GetType().Name}，Print参数：{content}");
    }
}

[AOTReflection]
public partial class Order : Parent
{
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime Birthday { get; set; }
    public string[] Hobbies { get; set; }
}

namespace APITest.Models
{
    [AOTReflection]
    public class Person : Parent
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime Birthday { get; set; }
        public string[] Hobbies { get; set; }

    }
}
```
Getの結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538010/69fcafa6-ac05-096a-8002-2aa64f0da4dc.png)

Postの結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538010/c536f454-bce7-65ae-ed89-c83705a48b98.png)
このコードを詳しく試すと、「可能な限り」という言葉の意味が理解できるかもしれません。なぜなら、.NETのリフレクションは非常に強力であるにもかかわらず、この方法では自分で定義したタイプのリフレクションの問題しか解決できないからです。これは、[AOTReflection]を使用して自定義タイプにハードコードされた方法で追加することによって完成します。しかし、このアプローチを提供することで、反映したいタイプの特徴を見つけ、ソースジェネレータ内でGetMembersをターゲットに呼び出すことができ、これにより制限を受けることはありません。

【GPTによる翻訳】