[Roslyn] オブジェクトインスタンス化コード生成に必要な要素の調査(おまけ）

# 前書き

[オブジェクトインスタンス化コード生成に必要な要素の調査](https://qiita.com/ktz_alias/items/cbeb74b6b5708bc875b4)で、オブジェクトインスタンス化に必要な最低限の要素を取得したが、せっかくなので取得結果を用いて、実際にインスタンス化のコード生成をユニットテストを書いて確認したので投下しておく。

# 確認内容

生成するコードとして、

* Nullable + コンストラクタをもつ構造体
* プロパティでの初期化が必要な構造体
* IEnumerable + 公開フィールドでの初期化が必要な構造体

の3パターンについて、これらの型を戻りとするメソッドを持つインターフェースでもって確認した。

意味解析の結果を取得する際、ASTを構築する必要があるため、そのあたりの諸々ののコードを静的クラス`SyntaxGeneratorHelper`として用意した([Roslynによるインターフェースの実装クラスの構築](https://qiita.com/ktz_alias/items/3cf626f1522311f3abf2)で作ったコードを叩き台にして切り出しただけ)。

```csharp
    public static class SyntaxGeneratorHelper {
        public static UsingDirectiveSyntax ToUsingDirective(string inUsing) {
            return 
                SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(inUsing).WithLeadingTrivia(SyntaxFactory.Space))
                .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
            ;
        }
        public static CSharpCompilation CreateCompilation(SyntaxTree inDaoAST, SyntaxTree inEntityAST) {
            var dotnetCoreDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();

            var opts = new CSharpCompilationOptions(
                outputKind: OutputKind.DynamicallyLinkedLibrary
            );

            return 
                CSharpCompilation.Create("autoGen", 
                    syntaxTrees: new[] { 
                        inDaoAST, inEntityAST                  
                    },
                    references: new[] {
                        AssemblyMetadata.CreateFromFile(typeof(object).Assembly.Location).GetReference(),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "netstandard.dll")),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "System.Runtime.dll")),
                    },
                    options: opts
                );
        }    
    }
}
```

# コード生成についてのユニットテスト

以下ソースコード生成を行うソースコード

```csharp
using NUnit.Framework;

using System;
using System.IO;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using GenSyntaxTestHelpers;
using SemSymbols;

namespace GenTypeInitializationTests
{
    public class GenTypeInitializationTest {
        [SetUp]
        public void Setup()
        {
        }

        private static readonly string IntfSource1 = @"
        namespace SemModels {
            public interface IColorDao1 {
                ColorData? FindById(int id);
            }
        }            
        ";
        private static readonly string EntitySource1 = @"
        namespace SemModels {
            public readonly struct ColorData {
                public int Id { get; }
                public string Name { get; }
                public int Red { get; }
                public int Green { get; }
                public int Blue { get; }

                public ColorData(int id, string name, int red = default, int green = default, int blue = default) => 
                    (Id, Name, Red, Green, Blue) = (id, name, red, green, blue);
            }    
        }    
        ";

        private static readonly string IntfSource2 = @"
        namespace SemModels {
            public interface IColorDao2 {
                ColorDataMut FindById(int id);
            }
        }            
        ";
        private static readonly string EntitySource2 = @"
        namespace SemModels {
            public struct ColorDataMut {
                public int Id { get; set; }
                public int Code { get => this.Id; }
                public string Name { get; set; }
                public int Red { get; set; }
                public int Green { get; set; }
                public int Blue { get; set; }
            }    
        }    
        ";

        private static readonly string IntfSource3 = @"
        using System.Collections.Generic;

        namespace SemModels {
            public interface IColorDao3 {
                IEnumerable<ColorDataMut2> FindAll();
            }
        }            
        ";

        private static readonly string EntitySource3 = @"
        namespace SemModels {
            public struct ColorDataMut2 {
                public int id;
                public string name;
                public int red;
                public int green;
                public int blue;
            }    
        }    
        ";

        [Test]
        public void _メソッドの実装_Nullableな戻り値_コンストラクタが適用される場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource1);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource1);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);
            
            var implTree = SyntaxFactory.ParseSyntaxTree(@"
            namespace SemModelsImpl {
                public class ColorDaoImpl: IColorDao1 {
                    
                }
            }   
            ");

            var classTree = 
                implTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .First()
            ;

            var method = intfTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
                .First()
            ;

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(method.ReturnType);  
            SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx);
            
            var parameters = ctx.Constructors[0].Symbols;
            var arg = method.ParameterList.Parameters[0].Identifier.Text;

            var implMethod = 
                method
                .WithSemicolonToken(default)
                .WithLeadingTrivia(SyntaxFactory.Space)
                .WithModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword).AsTokens())
                .WithBody(
                    SyntaxFactory.Block(
                        SyntaxFactory.ReturnStatement(
                                SyntaxFactory.ObjectCreationExpression(
                                    SyntaxFactory.IdentifierName(ctx.NamedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).WithLeadingTrivia(SyntaxFactory.Space),
                                    SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList<ArgumentSyntax>(new[] {
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[0].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.IdentifierName(arg)
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[1].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("cyan"))
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[2].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0))
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[3].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                        ),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.NameColon(parameters[4].Name), SyntaxFactory.Token(default), 
                                            SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                        ),
                                    })),
                                    null

                                )
                                .WithLeadingTrivia(SyntaxFactory.Space)
                        )
                    )
                )
            ;

            classTree = classTree.AddMembers(new[] { implMethod });

            var ns = (NamespaceDeclarationSyntax)implTree.GetCompilationUnitRoot().Members[0];
            var usings = new[] {
                "SemModels"
            };

            var newUnit = 
                SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(classTree.AsMemberDecls()))
                .WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList())
            ;

            var emitResult = this.EmitGenUnit("ConstructorEntityDao", newUnit, asm => {
                Assert.That(asm.GetTypes().Length, Is.EqualTo(1), "生成されたクラス数");

                var typeNames = asm.GetTypes().Select(t => t.FullName).ToArray();

                Assert.That(typeNames, Does.Contain("SemModelsImpl.ColorDaoImpl"), "生成された型名"); 

                var instance = (SemModels.IColorDao1)asm.CreateInstance("SemModelsImpl.ColorDaoImpl");

                var data = instance.FindById(4);

                Assert.That(data, Is.InstanceOf<System.Nullable<SemModels.ColorData>>(), "目的の値が生成されていること");
                Assert.That(data.HasValue, Is.True, "内包する型のインスタンスが生成されていること"); 
                Assert.That(data.Value.Id, Is.EqualTo(4), "渡したIdと一致していること");
                Assert.That(data.Value.Name, Is.EqualTo("cyan"), "色名");
                Assert.That(data.Value.Red, Is.EqualTo(0), "赤成分");
                Assert.That(data.Value.Green, Is.EqualTo(255), "緑成分");
                Assert.That(data.Value.Blue, Is.EqualTo(255), "青成分");
            });
            
            foreach (var d in emitResult.Diagnostics) {
                TestContext.Progress.WriteLine(d);
            }
            Assert.That(emitResult.Success, Is.True, "コンパイル結果");
        }

        [Test]
        public void _メソッドの実装_Nullableな戻り値_コプロパティによる初期化が適用される場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource2);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource2);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);
            
            var implTree = SyntaxFactory.ParseSyntaxTree(@"
            namespace SemModelsImpl {
                public class ColorDaoImpl2: IColorDao2 {
                    
                }
            }   
            ");

            var classTree = 
                implTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .First()
            ;

            var method = intfTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
                .First()
            ;

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(method.ReturnType);  
            SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx);
            
            var props = ctx.PropertyTypeVars.Symbols;
            var arg = method.ParameterList.Parameters[0].Identifier.Text;

            var implMethod =
                method
                .WithSemicolonToken(default)
                .WithLeadingTrivia(SyntaxFactory.Space)
                .WithModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword).AsTokens())
                .WithBody(
                    SyntaxFactory.Block(
                            SyntaxFactory.ReturnStatement(
                                SyntaxFactory.ObjectCreationExpression(
                                    SyntaxFactory.IdentifierName(ctx.NamedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).WithLeadingTrivia(SyntaxFactory.Space),
                                    SyntaxFactory.ArgumentList(),
                                    SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression,
                                        SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[0].Name),
                                                SyntaxFactory.IdentifierName(arg)
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[1].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("magenta"))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[2].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[3].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(props[4].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            )
                                        })
                                    )
                                )
                                .WithLeadingTrivia(SyntaxFactory.Space)
                            )
                    )
                )
            ;


            classTree = classTree.AddMembers(new[] { implMethod });

            var ns = (NamespaceDeclarationSyntax)implTree.GetCompilationUnitRoot().Members[0];
            var usings = new[] {
                "SemModels"
            };

            var newUnit = 
                SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(classTree.AsMemberDecls()))
                .WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList())
            ;

            var emitResult = this.EmitGenUnit("PropertyEntityDao", newUnit, asm => {
                Assert.That(asm.GetTypes().Length, Is.EqualTo(1), "生成されたクラス数");

                var typeNames = asm.GetTypes().Select(t => t.FullName).ToArray();

                Assert.That(typeNames, Does.Contain("SemModelsImpl.ColorDaoImpl2"), "生成された型名"); 

                var instance = (SemModels.IColorDao2)asm.CreateInstance("SemModelsImpl.ColorDaoImpl2");

                var data = instance.FindById(5);

                Assert.That(data, Is.InstanceOf<SemModels.ColorDataMut>(), "目的の値が生成されていること");
                Assert.That(data.Id, Is.EqualTo(5), "渡したIdと一致していること");
                Assert.That(data.Name, Is.EqualTo("magenta"), "色名");
                Assert.That(data.Red, Is.EqualTo(255), "赤成分");
                Assert.That(data.Green, Is.EqualTo(0), "緑成分");
                Assert.That(data.Blue, Is.EqualTo(255), "青成分");            
            });
            
            foreach (var d in emitResult.Diagnostics) {
                TestContext.Progress.WriteLine(d);
            }
            Assert.That(emitResult.Success, Is.True, "コンパイル結果");
        }


        [Test]
        public void _メソッドの実装_Nullableな戻り値_公開フィールドによる初期化が適用される場合() {
            var intfTree = SyntaxFactory.ParseSyntaxTree(IntfSource3);
            var entityTree = SyntaxFactory.ParseSyntaxTree(EntitySource3);

            var compiler = SyntaxGeneratorHelper.CreateCompilation(intfTree, entityTree);
            
            var implTree = SyntaxFactory.ParseSyntaxTree(@"
            namespace SemModelsImpl {
                public class ColorDaoImpl3: IColorDao3 {
                    
                }
            }   
            ");

            var classTree = 
                implTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .First()
            ;

            var method = intfTree.GetCompilationUnitRoot()
                .DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
                .First()
            ;

            var model = compiler.GetSemanticModel(intfTree);
            var entityInfo = model.GetTypeInfo(method.ReturnType);  
            SemSymbolHelper.TryCResolveReturnTypeContext(entityInfo, out var ctx);
            
            var fields = ctx.FieldTypeVars.Symbols;

            var implMethod =
                method
                .WithSemicolonToken(default)
                .WithLeadingTrivia(SyntaxFactory.Space)
                .WithModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword).AsTokens())
                .WithBody(
                    SyntaxFactory.Block(
                            SyntaxFactory.YieldStatement(
                                SyntaxKind.YieldReturnStatement,
                                SyntaxFactory.Token(SyntaxKind.YieldKeyword),
                                SyntaxFactory.Token(SyntaxKind.ReturnKeyword).WithLeadingTrivia(SyntaxFactory.Space),                                
                                SyntaxFactory.ObjectCreationExpression(
                                    SyntaxFactory.IdentifierName(ctx.NamedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)).WithLeadingTrivia(SyntaxFactory.Space),
                                    SyntaxFactory.ArgumentList(),
                                    SyntaxFactory.InitializerExpression(SyntaxKind.ObjectInitializerExpression,
                                        SyntaxFactory.SeparatedList<ExpressionSyntax>(new[] {
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[0].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(5))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[1].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("yellow"))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[2].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[3].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(255))
                                            ),
                                            SyntaxFactory.AssignmentExpression(
                                                SyntaxKind.SimpleAssignmentExpression, 
                                                SyntaxFactory.IdentifierName(fields[4].Name),
                                                SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0))
                                            )
                                        })
                                    )
                                )
                                .WithLeadingTrivia(SyntaxFactory.Space),
                                SyntaxFactory.Token(SyntaxKind.SemicolonToken)
                            )
                    )
                )
            ;

            classTree = classTree.AddMembers(new[] { implMethod });

            var ns = (NamespaceDeclarationSyntax)implTree.GetCompilationUnitRoot().Members[0];
            var usings = new[] {
                "System.Collections.Generic",
                "SemModels"
            };

            var newUnit = 
                SyntaxFactory.CompilationUnit().AddMembers(ns.WithLeadingTrivia(null).WithMembers(classTree.AsMemberDecls()))
                .WithUsings(usings.Select(SyntaxGeneratorHelper.ToUsingDirective).ToSyntaxList())
            ;

            var emitResult = this.EmitGenUnit("FieldEntityDao", newUnit, asm => {
                Assert.That(asm.GetTypes().Length, Is.EqualTo(2), "生成されたクラス数(自動生成されたIteratorを含むため)");

                var typeNames = asm.GetTypes().Select(t => t.FullName).ToArray();

                Assert.That(typeNames, Does.Contain("SemModelsImpl.ColorDaoImpl3"), "生成された型名"); 

                var instance = (SemModels.IColorDao3)asm.CreateInstance("SemModelsImpl.ColorDaoImpl3");

                var seq = instance.FindAll();

                Assert.That(seq, Is.InstanceOf<IEnumerable<SemModels.ColorDataMut2>>(), "目的の値が生成されていること");
                Assert.That(seq.Any(), Is.True, "内包する型のインスタンスが生成されていること");
                Assert.That(seq.Count(), Is.EqualTo(1), "要素数");

                var data = seq.First();

                Assert.That(data.id, Is.EqualTo(5), "渡したIdと一致していること");
                Assert.That(data.name, Is.EqualTo("yellow"), "色名");
                Assert.That(data.red, Is.EqualTo(255), "赤成分");
                Assert.That(data.green, Is.EqualTo(255), "緑成分");
                Assert.That(data.blue, Is.EqualTo(0), "青成分");            
            });
            
            foreach (var d in emitResult.Diagnostics) {
                TestContext.Progress.WriteLine(d);
            }
            Assert.That(emitResult.Success, Is.True, "コンパイル結果");
        }

        private EmitResult EmitGenUnit(string inAsmName, CompilationUnitSyntax inGenTree, Action<Assembly> inCallback) {
            using(var stream = new MemoryStream()) {
                var dotnetCoreDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();

                var opts = new CSharpCompilationOptions(
                    outputKind: OutputKind.DynamicallyLinkedLibrary
                );

                var newCompilation = CSharpCompilation.Create(inAsmName, 
                    syntaxTrees: new[] { SyntaxFactory.SyntaxTree(inGenTree) },
                    references: new[] {
                        AssemblyMetadata.CreateFromFile(typeof(object).Assembly.Location).GetReference(),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "netstandard.dll")),
                        MetadataReference.CreateFromFile(Path.Combine(dotnetCoreDirectory, "System.Runtime.dll")),
                        AssemblyMetadata.CreateFromFile(this.GetType().Assembly.Location).GetReference(),
                    },
                    options: opts
                );

                var emitResult = newCompilation.Emit(stream);
                
                if (emitResult.Success) {
                    Assert.That(inCallback, Is.Not.Null);

                    stream.Position = 0;
                    var buf = new byte[stream.Length];
                    stream.Read(buf, 0, buf.Length);

                    var asm = Assembly.Load(buf);

                    inCallback(asm);
                }
                
                return emitResult;
            }
        }
    }
}

namespace SemModels {
    public readonly struct ColorData {
        public int Id { get; }
        public string Name { get; }
        public int Red { get; }
        public int Green { get; }
        public int Blue { get; }

        public ColorData(int id, string name, int red = default, int green = default, int blue = default) => 
            (Id, Name, Red, Green, Blue) = (id, name, red, green, blue);
    }    

    public interface IColorDao1 {
        ColorData? FindById(int id);
    }

    public struct ColorDataMut {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Red { get; set; }
        public int Green { get; set; }
        public int Blue { get; set; }
    }    

    public interface IColorDao2 {
        ColorDataMut FindById(int id);
    }

    public struct ColorDataMut2 {
        public int id;
        public string name;
        public int red;
        public int green;
        public int blue;
    }    

    public interface IColorDao3 {
        IEnumerable<ColorDataMut2> FindAll();
    }
}
```

# 追記
テストコードにコメントアウトしたゴミを拾い集めコード生成の手引きとしてまとめる

`Roslyn`の@SyntaxFactory`はアホほどメソッドが生えててどれを使ったいいか結構迷う。

その際の手引きとして、

```csharp
var body = SyntaxFactory.ParseCompilationUnit("public SemModels.ColorData x(int id) { return new SemModels.ColorData() { Red = 128 }; }");
foreach (var n in body.DescendantNodesAndTokensAndSelf()) {
   TestContext.Progress.WriteLine($"{n.Kind().ToString().PadRight(30)}{n.ToFullString()}");
}
```

のように文字列から構文木を実体化させ、コンソールに出力してみるのが近道かも。
(NUnitの場合、`TestContext.Progress.WriteLine`メソッドで、出力してくれる)

また、組み立てた構文木についても、`ToFullString()`メソッドを呼ぶことで文字列に戻すことができる（インデントはめちゃくちゃな可能性あり）

```csharp
TestContext.Progress.WriteLine(newUnit.ToFullString());
```