.net8：インターセプターInterceptors

C#12では、インターセプターが導入されましたが、実験的な機能として提供されているため、初めて見たときは記事を書かずにいました。しかし、最近AOTを調べていると、Dapper.AOTがこの機能を利用していることがわかり、情報を整理し、共有したいと思いました。将来、この機能が変更されたり、削除されたりした場合は、この記事を無視してください。

以下は、マイクロソフトの公式ドキュメントからのヒントです：
インターセプターは実験的な機能であり、C# 12のプレビューモードで提供されます。将来のバージョンでは、この機能が大きく変更されるか、削除される可能性があります。したがって、本番環境やリリース済みのアプリケーションでの使用は推奨されません。

実際、インターセプターの実装は比較的シンプルで、拡張メソッドを定義し、他のメソッドを置き換えるだけです。置き換えるメソッドはInterceptsLocationで指定され、拡張メソッドと元のメソッドのシグネチャが一致している必要があります。ここで注意が必要なのは、プロジェクト内でInterceptsLocationAttributeを定義する必要があることです。コードは以下の通りです：

```

using System.Runtime.CompilerServices;

var myclass = new MyClass();
myclass.Print("测试");
myclass.Print("测试");


public class MyClass
{
    public void Print(string s)
    {
        Console.WriteLine($"MyClass.Print({s})");
    }
}
namespace Interceptors
{
    public static class MyClassIntercepts
    {
        [InterceptsLocation("C:\\MyFile\\Source\\Repos\\Asp.NetCoreExperiment\\Asp.NetCoreExperiment\\Interceptors\\InterceptorsDemo\\Program.cs", 5, 9)]
        public static void InterceptorPrint(this MyClass myclass, string s)
        {
            Console.WriteLine($"ABC.AOT下 MyClass.InterceptorPrint 拦截 MyClass.Print方法，参数是：{s}");
        }
    }
}

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute
    {
    }
}
```

インターセプターを使用する場合は、プロジェクトファイル(.csproj)にInterceptorsPreviewNamespacesを追加して、インターセプターに関する情報を明示する必要があります。

```
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Interceptors</InterceptorsPreviewNamespaces>
  </PropertyGroup>
</Project>
```

インターセプターは比較的シンプルで、コンパイル時に置き換えを行うものです。この使用法は、自然とC#のソースジェネレーターを思い出させます。ソースジェネレーターとは何か？以下の例を参照してください。

まず、TypeMessageGeneratorというソースジェネレーターのプロジェクトがあり、プロジェクトタイプを変更し、以下のようにNugetパッケージを導入します：

```
<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.3.4">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
  </ItemGroup>
</Project>
```

コードの実装は比較的シンプルで、この例ではプロジェクト内のいくつかのクラス、クラスメンバーなどの情報を外部ファイルに生成します。

```
using Microsoft.CodeAnalysis;
namespace TypeMessageGenerator{
    [Generator]
    public class TypeMembersSourceGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

            var path = "C:\\MyFile\\abc\\typemembers.txt";
            File.WriteAllText(path, "");
            File.AppendAllText(path, context.Compilation.Assembly.Name + "\r\n");
            File.AppendAllText(path, "========================\r\n");

            foreach (var typename in context.Compilation.Assembly.TypeNames)
            {
                File.AppendAllText(path, typename + "\r\n");
                try
                {
                    var mytype = context.Compilation.Assembly.GetTypeByMetadataName(typename);
                    if (mytype == null)
                    {
                        mytype = context.Compilation.Assembly.GetTypeByMetadataName($"{context.Compilation.Assembly.Name}.{typename}");
                    }
                    if (mytype == null)
                    {
                        continue;
                    }

                    foreach (var member in mytype.GetMembers())
                    {
                        try
                        {
                            File.AppendAllText(path, $"-- {member.Name} {member.Kind} \r\n");
                            foreach (var location in member.Locations)
                            {
                                try
                                {
                                    File.AppendAllText(path, $"---- {location.GetLineSpan().StartLinePosition.Line},{location.GetLineSpan().EndLinePosition.Character} {location.GetLineSpan().Path}\r\n");
                                }
                                catch (Exception exc)
                                {
                                    File.AppendAllText(path, $"1   {exc.Message} \r\n");
                                }
                            }
                        }
                        catch (Exception exc)
                        {
                            File.AppendAllText(path, $"2   {exc.Message} \r\n");
                        }
                    }
                }
                catch (Exception exc)
                {
                    File.AppendAllText(path, $"3   {exc.Message} \r\n");
                }
            }
        }

        public void Initialize(GeneratorInitializationContext context)
        {
        }
    }
}
```

どのように使用するか？ここでは、コンソールプロジェクトを定義し、上述のTypeMessageGeneratorプロジェクトへのプロジェクト参照を追加し、OutputItemType="Analyzer"およびReferenceOutputAssembly="false"の2つの属性を追加します。

```
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\TypeMessageGenerator\TypeMessageGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>
```

コンソールプロジェクトにPerson.csファイルを追加し、内容は以下の通りです。

```
namespace UserDemoTypeMessageGenerator{
    public class Person
    {
        public int ID { get; set; }

        public int Name { get; set; }

        public void PrintPerson()
        {
        }
    }
}
```

Program.csには手を加えずにプロジェクトをビルドするだけで、C:\\MyFile\\abcにtypemembers.txtが生成されます。内容は以下の通りです：

```
UserDemoTypeMessageGenerator
========================
Program
-- <Main>$ Method ---- 0,0 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Program.cs
-- .ctor Method ---- 0,7 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Program.cs
Person
-- <ID>k__BackingField Field ---- 4,21 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- ID Property ---- 4,21 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- get_ID Method ---- 4,27 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- set_ID Method ---- 4,32 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- <Name>k__BackingField Field ---- 6,23 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- Name Property ---- 6,23 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- get_Name Method ---- 6,29 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- set_Name Method ---- 6,34 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- PrintPerson Method ---- 8,31 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
-- .ctor Method ---- 2,23 C:\MyFile\Source\Repos\Asp.NetCoreExperiment\Asp.NetCoreExperiment\SourceGenerator\UserDemoTypeMessageGenerator\Person.cs
```

細かく見ると、生成されたこのTxtの----の後には、2つの数字と1つのパスがあります。これはインターセプターと同じではないでしょうか？何か考えがありますか？

もし考えがなければ、2023年12月16日午後のB会場での共有に参加してください。この会場では、李先生（李卫涵）が16:10にインターセプターについて、また私がAOT実践について共有し、あなたの疑問を解消することができるでしょう。

![alt 画像](https://mmbiz.qpic.cn/sz_mmbiz_jpg/4CAA63nvHRClP54rGmvhsyichl0kls0PL8XZick2CCFdiaIC49P51sMrso3OicqGAE2FoBahY8rCeZ6BOjRapZalWA/640?wx_fmt=jpeg&from=appmsg)

(Translated by GPT)