ねののお庭。

かりかりもふもふ。

【C#】GetTypeByMetadataName ではなく GetTypesByMetadataName を使った方が無難かもしれない。

Type か Types かの違い。

何の話だよ、っていうと Roslyn の API のお話です。Source Generator を開発する場合は Roslyn と踊る事になるので今はまだ Roslyn 触ってない人でも頭の片隅に置いておくといいんじゃないでしょうか。

さて、型のシンボルを取得するのには幾つかの方法がありますが、既知の型のシンボルとかを取得する場合、以下のように書くのが普通です。

INamedTypeSymbol? symbol = compilation.GetTypeByMetadataName("System.DateTime");

標準ライブラリの中から拾ってくる場合はこれで問題ありません。 しかし、標準ライブラリでない場合、問題が発生する可能性が僅かながらあります。

私は Tapper という C# の型を TypeScript の型に変換するライブラリを OSS として公開しているのですが、そこで実際に踏んだ例を紹介します。

Tapper では、[TranspilationSource]という属性をつけて、変換対象の C# の型を指定します。

[TranspilationSource] // <- Add attribute!
public class SampleType
{
    public List<int>? List { get; init; }
    public int Value { get; init; }
    public Guid Id { get; init; }
    public DateTime DateTime { get; init; }
}

なので、ユーザが定義した型に、[TranspilationSource] という属性が当たっているかを判別する必要があります。 そのため、当然ながら以下のように属性のシンボルを取得するプロセスを踏んでいます。

INamedTypeSymbol? attributeSymbol = compilation.GetTypeByMetadataName("Tapper.TranspilationSourceAttribute");

ここで問題が発生しました。attributeSymbol が常に null なのです...! 属性が含まれる nuget パッケージは参照され、コンパイルも問題なく通るので [TranspilationSource] が compilation に含まれてないわけがないのに、null が返ってくるのです。 これでは[TranspilationSource]がアノテーションされた型を見つけ出す事ができません。

そしてやっかいな事に、実際に開発しているアプリケーションを対象に Tapper を用いると、Tapper 内部で呼んでいる compilation.GetTypeByMetadataName("Tapper.TranspilationSourceAttribute") は null が返ってくる一方で、Tapper の検証用のシンプルなプロジェクトでは null ではなく、ちゃんとシンボルが返ってきていました。宇宙猫になってましたね、この時。

この原因は MetadataName だけだと曖昧だったからでした。 曖昧ってなんぞや、というと同一名前空間、同一名の型が別々のプロジェクトに含まれている場合などに発生します。 標準ライブラリには入ってない well-known types (例えば AsyncLock とか) で発生する可能性が大いにあります。 これが何故 [TranspilationSource] で発生したかというと、パッケージ参照無しに属性を使いたかったから internal で同一名前空間、同一名で属性を定義しているプロジェクトがあったためでした。なるほどね...。

解決策としては、GetTypeByMetadataName ではなく、GetTypesByMetadataName を使おう、という事です。

// before
INamedTypeSymbol? attributeSymbol = compilation.GetTypeByMetadataName("Tapper.TranspilationSourceAttribute");

// after
ImmutableArray<INamedTypeSymbol> attributeSymbols = compilation.GetTypesByMetadataName("Tapper.TranspilationSourceAttribute");

これで attributeSymbols には曖昧なその全てのシンボルが含まれるようになります。これで大解決、[TranspilationSource]がアノテーションされた型をちゃんと見つけ出せるようになりました。 (Tapper はすでに GetTypeByMetadataName ではなく、GetTypesByMetadataName を使うようにしたバージョンをリリース済みです。)

ちなみに、GetTypesByMetadataName は Roslyn v4.2.0 以降に追加されている比較的新しめの API です。 Roslyn v4.2.0 がどのタイミングのものかというと、.NET 6 時代の途中にリリースされたモノです。 (この記事を書いている時は .NET 7 時代で、Roslyn v4.4.0 が最新です。)

Roslyn v4.1.0 から v4.2.0 にあがるタイミングで GetTypeByMetadataName に書かれている docs がめっちゃ増強されています。

これが Roslyn 4.1.0 の GetTypeByMetadataName

これが Roslyn 4.2.0 以降の GetTypeByMetadataName の説明。めっちゃ説明増えとる...!

ちなみに GetTypesByMetadataName はこんな感じ。

というわけで、GetTypeByMetadataName ではなく GetTypesByMetadataName を使った方が無難かもしれない、というお話でした。

References