T4でImmutableクラスコンストラクタ作成(C#)

C#(7.0)でImmutableクラスを作成する際、下記のような記述が必要になります。

public class Person
{
    public string Name { get; }
    public int Age { get; }
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

F#のレコードと異なり、コンストラクタを定義しないといけません。

今回はこのコンストラクタをT4で自動生成してみたいと思います。

Tangible T4

tangible T4 Editor for VS2010 / VS2012 / VS2013 / VS2015 / VS2017

素のVisual Studioでは、T4はハイライトや補間などが一切サポートされていません。

そこでこれらを有効にするためにTangible T4をインストールします。

有償版ではさらに、DTE関連のインテリセンスの制限が解除されたり、デバッグができるようになるようです。

C#側準備

ターゲットのクラス判別のため、Record属性を定義しています。

またターゲットのクラスはpartialにします。

public class RecordAttribute : Attribute { }

[Record]
public partial class Person
{
    public string Name { get; }
    public int Age { get; }
}


T4ファイル

DTEを用いて、csファイル内のクラスを解析していきます。

this.Hostを利用するため、hostspecificはtrueにします。

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="EnvDTE" #>
<#@ output extension=".cs" #>
<#
    DTE dte = (this.Host as IServiceProvider).GetService(typeof(DTE)) as DTE;
    Project project = dte.Solution.FindProjectItem(this.Host.TemplateFile).ContainingProject; // T4ファイルが所属するプロジェクト
#>



<# 
    foreach (CodeClass cls in GetClasses(project, "Record")) { 
        string parameters = string.Join(", ", 
            GetProperties(cls).Select(x => string.Format("{0} {1}", x.Type.AsString, x.Name.ToLower()))); 
#>
namespace <#= cls.Namespace.FullName #>
{
    public partial class <#= cls.Name #>
    {
        public <#= cls.Name #> (<#= parameters #>)
        {
<# foreach (CodeProperty p in GetProperties(cls)) { #>
            <#= p.Name #> = <#= p.Name.ToLower() #>;
<# } #>
        }
    }
}
<# } #>



<#+ 
IEnumerable<T> Flatten<T>(IEnumerable<T> root, Func<T, IEnumerable<T>> getChildren)
{
    // 木構造を配列に展開
    return root.Concat(root.SelectMany(x => Flatten(getChildren(x), getChildren)));
}
IEnumerable<ProjectItem> GetCSFiles(Project project)
{
    return Flatten(project.ProjectItems.Cast<ProjectItem>(), x => x.ProjectItems.Cast<ProjectItem>())
        .Where(x => Path.GetExtension(x.Name) == ".cs");
}
IEnumerable<CodeClass> GetClasses(ProjectItem item)
{
    // 今回はnamespace直下のクラスのみ取得
    return Flatten(item.FileCodeModel.CodeElements.Cast<CodeElement>(),
        x => x is CodeNamespace
            ? (x as CodeNamespace).Members.Cast<CodeElement>()
            : Enumerable.Empty<CodeElement>())
        .OfType<CodeClass>();
}
IEnumerable<CodeClass> GetClasses(Project project, string attribute)
{
    return GetCSFiles(project)
        .SelectMany(GetClasses)
        .Where(c => c.Attributes.Cast<CodeAttribute>().Any(x => x.Name == attribute));
}
IEnumerable<CodeProperty> GetProperties(CodeClass cls)
{
    // { get; }なのか、{ get { return ユーザー定義 } }なのかの違いがCodePropertyから判別できないため、今回は文字列解析で判別
    return cls.Members
        .OfType<CodeProperty>()
        .Where(x => x.StartPoint.CreateEditPoint().GetText(x.EndPoint).TrimEnd().EndsWith("{ get; }"));
}
#>


自動生成結果

上記T4により、下記のクラスが自動生成されます。

public partial class Person
{
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}