T4 でカレントプロジェクトの参照アセンブリの情報を取得する


## このドキュメントの内容

`T4` でテキスト生成を行う際、テキストテンプレートを含むカレントプロジェクトの情報を参照したくなるケースがあります。プロジェクトが参照しているアセンブリの情報を取得する方法を紹介します。この例では Name と Path の値のみを対象としていますが、PublicToken や Version などの値を取得することもできます。

参考 VSLangProj.Reference インターフェース
https://docs.microsoft.com/en-us/dotnet/api/vslangproj.reference?view=visualstudiosdk-2017

VisualStudio 2017 以降ではパッケージの管理方法に PackageReference 形式が推奨されています。プロジェクトファイルから参照パスを取得することが難しくなっていますが、NuGet で参照したアセンブリの参照パスをこの方法で取得できることを確認しました。

私の目的はこの方法で取得した参照パスをテンプレートの assembly ディレクティブに指定することでしたが、それは実現できませんでした。

### 参照アセンブリを取得するインクルード用テンプレート

指定されたプロジェクトの参照アセンブリの名前とパスを取得する GetReferences メソッドを定義しています。
プロジェクト情報の取得には EnvDTE と VSLangProj を使っています。
ホストを参照するにはテンプレートの hostspecific プロパティに true を設定する必要があります。
このテンプレートは単独でテキスト生成に使用するわけではないため、拡張子を tt ではなく t4 にしています。

念のため、Marshal.FinalReleaseComObject メソッドで COM オブジェクトの解放を行っています。DTE と IServiceProvider に対して解放を行うと「基になる RCW から分割された COM オブジェクトを使うことはできません。」例外が発生するため、対象外としています。

```c#:DTETemplate.t4
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="VSLangProj" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="VSLangProj" #>
<#@ import namespace="System.Runtime.InteropServices" #>
<# 

void ReleaseComObjects(params object[] objects) {
	foreach (object o in objects) {
	try {
		if (o == null) { continue; }
		if (!Marshal.IsComObject(o)) { continue; }
			Marshal.FinalReleaseComObject(o);
		} catch (Exception) {
		}
	}
}

IDictionary<string, string> GetReferences(string projectName)
{
	Dictionary<string, string> dic = new Dictionary<string, string>();

	IServiceProvider service = (IServiceProvider)this.Host;
	DTE dte = service.GetService(typeof(DTE)) as DTE;
	Solution solution = null;

	try
	{
		solution = dte.Solution;

		foreach (Project project in solution.Projects)
		{
			VSProject vsProject = null;
			References refs = null;

			try
			{
				if (project.Name != projectName) { continue; }

				vsProject = (VSProject)project.Object;
				if (vsProject == null) { continue; }

				refs = vsProject.References;
				if ( refs == null ) { continue; }

				foreach (Reference reference in refs)
				{
					dic.Add(reference.Name, reference.Path);
					ReleaseComObjects(reference);
				}
			}
			finally
			{
				ReleaseComObjects(refs, vsProject, project);
			}
		}
	}
	finally
	{
		ReleaseComObjects(solution);
	}

	return dic;
}
#>
```

### テンプレート例

上記の GetReferences メソッドを呼び出す例です。
DTETemplate.t4 をインクルードしています。

```c#:Template1.tt
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".txt" #>
<#@ include file="DTETemplate.t4" #>
<#

// CodeGenerationSample プロジェクトの参照アセンブリの名前とパスを取得
IDictionary<string, string> refs = GetReferences("CodeGenerationSample");

foreach (string name in refs.Keys)
{
	WriteLine(string.Format("{0} = {1}", name, refs[name]));
}

#>
```

### 出力結果

mxProject.CodeGenerations は NuGet を使って参照しています。NuGet のリポジトリのパスが取得できています。

```text:Template1.txt
NETStandard.Library = 
System.Security.Cryptography.Algorithms = C:\Program Files\dotnet\sdk\NuGetFallbackFolder\netstandard.library\2.0.3\build\netstandard2.0\ref\System.Security.Cryptography.Algorithms.dll
System.Net.Requests = C:\Program Files\dotnet\sdk\NuGetFallbackFolder\netstandard.library\2.0.3\build\netstandard2.0\ref\System.Net.Requests.dll
System.Windows = C:\Program Files\dotnet\sdk\NuGetFallbackFolder\netstandard.library\2.0.3\build
\netstandard2.0\ref\System.Windows.dll
（省略）
mxProject.CodeGenerations = E:\Data\Program\Nuget\global\mxproject.codegenerations\0.5.0\lib\netstandard2.0\mxProject.CodeGenerations.dll
```