May 2017
Volume 32 Number 5
.NET Core - Roslyn と .NET Core によるクロスプラットフォーム コードの生成
.NET Core は、モジュール方式を採用した、クロスプラットフォーム対応のオープン ソース ツール セットです。このツールによって、Windows、Linux、macOS で実行する次世代 .NET アプリケーションをビルドできます (microsoft.com/net/core/platform、英語)。また、Windows 10 IoT ディストリビューションにインストールできるため、Raspberry Pi などのデバイスでも実行できます。.NET Core は、ランタイム、ライブラリ、コンパイラに加え、C#、F#、Visual Basic などの言語への完全サポートを備える強力なプラットフォームです。このことは、「Project Roslyn」とも呼ばれる .NET コンパイラ (github.com/dotnet/roslyn、英語) によって、リッチなコード分析 API を備えたクロスプラットフォームのオープン ソース コンパイラが提供されるため、Windows だけでなく、さまざまな OS でも C# でコーディングできるようになることを意味します。重要なのは、Roslyn API を利用して、多種多様な OS で、コード分析、コード生成、コンパイルなどの多くのコード関連操作を実行できることです。今回は、Roslyn API を使用する C# プロジェクトを .NET Core でセットアップするのに必要な手順のチュートリアルを提供します。さらに、コード生成やコンパイルに関する興味深いシナリオをいくつか紹介します。また、Roslyn を使ってコンパイルされたコードを .NET Core で呼び出して実行するための基本的なリフレクション技法も説明します。Roslyn に詳しくない方は、以下の記事を参考にしてください。
- 「Roslyn を使用した API 向けライブ コード アナライザーの作成」(msdn.com/magazine/dn879356)
- 「Roslyn アナライザーへのコード修正の追加」(msdn.com/magazine/dn904670)
- 「Roslyn でモデル - ビュー - ビューモデルを使いやすくする」(msdn.com/magazine/mt703435)
.NET Core SDK のインストール
まず、.NET Core と SDK をインストールします。Windows で作業していて Visual Studio 2017 をインストールしている場合、インストール時に Visual Studio インストーラーで [.NET Core クロスプラットフォームの開発] ワークロードを選択していれば、.NET Core が既に含まれています。選択していない場合は、Visual Studio インストーラーを開き、上記のワークロードを選択して、[変更] をクリックするだけです。Windows で作業していても Visual Studio 2017 をインストールしていない場合や、Linux や macOS を使用している場合、.NET Core を手動でインストールするか、Visual Studio Code を開発環境として使用することができます (code.visualstudio.com、英語)。Visual Studio Code はそれ自体がクロスプラットフォームで、.NET Core との連携が優れているため、今回はこちらのシナリオを取り上げます。加えて、Visual Studio Code 用の C# 拡張機能も忘れずにインストールします (bit.ly/29b1Ppl、英語)。.NET Core をインストールする手順は OS ごとに異なるため、bit.ly/2mJArWx、英語) の指示に従ってください。必ず最新のリリースをインストールします。.NET Core の最新リリースでは project.json ファイル形式がサポートされなくなりましたが、代わりに、より一般的な MSBuild の .csproj file 形式がサポートされます。
C# での .NET Core アプリケーションのスキャフォールディング
.NET Core により、コンソール アプリケーションや Web アプリケーションを作成できます。Web アプリケーションの場合、マイクロソフトは .NET Core のロードマップを進めながら、ASP.NET Core テンプレート以外にも、多くのテンプレートを利用可能にしています。Visual Studio Code のエディターは簡易版のため、Visual Studio ほどプロジェクト テンプレートが提供されていません。そのため、アプリケーションと同じ名前のフォルダー内に、コマンド ラインからアプリケーションを作成する必要があります。以下の例は、Windows の指示に基づいていますが、macOS と Linux にも同じ考え方を当てはめることができます。まず、コマンド プロンプトを開いて、ディスク上のフォルダーに移動します。たとえば、C:\Apps というフォルダーがあるとして、次のコマンドを使用して、このフォルダーに移動して、RoslynCore という新しいサブフォルダーを作成します。
> cd C:\Apps
> md RoslynCore
> cd RoslynCore
したがって、今回紹介するサンプル アプリケーションの名前は、RoslynCore です。今回はコンソール アプリケーションです。コンソール アプリケーションは、Roslyn を使用したコーディングへのアプローチがシンプルになるため、チュートリアルには最適です。また、同じテクニックを、ASP.NET Core Web アプリケーションに当てはめることもできます。コンソール アプリケーション用に新しい空のプロジェクトを作成するには、以下のコマンド ラインを入力します。
> dotnet new console
こうすることで、RoslynCore というコンソール アプリケーション用の C# プロジェクトが .NET Core によってスキャフォールディングされます。これで、プロジェクトのフォルダーを Visual Studio Code から開けるようになります。最も簡単な方法は、以下のコマンド ラインを入力することです。
> code .
当然、Windows のスタート メニューから Visual Studio Code を開いて、プロジェクト フォルダーを手動で開いてもかまいません。C# コード ファイルを開くと、いくつか必要なアセットを生成し、NuGet パッケージを復元する権限を求めるメッセージが表示されます (図 1 参照)。
図 1 プロジェクトの更新を求める Visual Studio Code
次に、Roslyn での作業に必要な NuGet パッケージを追加します。
Roslyn NuGet パッケージの追加
Roslyn API を使用するには、Microsoft.CodeAnalysis 階層からいくつか NuGet パッケージをインストールします。これらのパッケージをインストールする前に、Roslyn API が .NET Core システムにいかに適合するかを明らかにしておくことが重要です。.NET Framework で Roslyn を扱った経験がある方は、Roslyn API のフル セットを使用したことがあるかもしれません。しかし、.NET Core は .NET 標準ライブラリに依存するため、.NET Core で利用できるのは、.NET 標準をサポートする Roslyn ライブラリだけです。本稿執筆時点では、Compiler API (Emit API と Diagnostic API を含む) や Workspaces API など、ほとんどの Roslyn API が .NET Core で既に利用できるようになっています。ごく一部の API には移植性がありません。しかし、マイクロソフトは Roslyn と .NET Core に多大な投資を行っているため、今後のリリースでは .NET 標準との完全な互換性が確保されることは、十分期待できます。.NET Core で実行されるクロスプラットフォーム アプリケーションの実例が OmniSharp (bit.ly/2mpcZeF、英語) です。これは、Roslyn API を利用して、入力候補の一覧表示機能や構文のハイライト機能など、コード エディター機能の大半を強化します。
ここでは、Compiler API と Diagnostic API を利用する方法を見ていきます。このためには、Microsoft.CodeAnalysis.CSharp NuGet パッケージをプロジェクトに追加する必要があります。MSBuild に基づく新しい .NET Core プロジェクト システムでは、NuGet パッケージの一覧が .csproj プロジェクト ファイルに含まれるようになります。Visual Studio 2017 では、NuGet パッケージのダウンロード、インストール、管理をクライアント UI から行えますが、Visual Studio Code にはこれに相当するオプションがありません。さいわい、単純に .csproj ファイルを開いて、必要な NuGet パッケージを表す <PackageReference> 要素を含む <ItemGroup> ノードを検索できます。見つけたノードを以下のように変更します。
<ItemGroup>
...
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"
Version="2.0.0 " />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
Microsoft.CodeAnalysis.CSharp パッケージへの参照を追加すると、C# コンパイラの API にアクセスできるようになります。また、System.Runtime.Loader パッケージはリフレクションに必要になるため、後ほど使用します。
変更を保存すると、Visual Studio Code が不足している NuGet パッケージを検出し、それらを復元するよう提案します。
コード分析: ソース テキストの解析と構文ノードの生成
最初の例はコード分析に関係します。この例ではソース コードを解析して新しい構文ノードを生成する方法を示します。たとえば、以下のシンプルなビジネス オブジェクトがあり、これを基にビュー モデル クラスを生成するとします。
namespace Models
{
public class Item
{
public string ItemName { get; set }
}
}
このビジネス オブジェクトのテキストのソースは、C# コード ファイル、コード内の文字列やユーザー入力文字列など、さまざまです。コード分析 API では、このソース テキストを解析して、コンパイラが理解して操作できる新しい構文ノードを生成できます。たとえば、クラス定義を含む文字列を解析し、それに対応する構文ノードを取得して、その構文ノードからビュー モデルを生成する新しい静的メソッドを呼び出すコードを考えます (図 2 参照)。
図 2 ソース コードの解析と構文ノードの取得
using System;
using RoslynCore;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
class Program
{
static void Main(string[] args)
{
GenerateSampleViewModel();
}
static void GenerateSampleViewModel()
{
const string models = @"namespace Models
{
public class Item
{
public string ItemName { get; set }
}
}
";
var node = CSharpSyntaxTree.ParseText(models).GetRoot();
var viewModel = ViewModelGeneration.GenerateViewModel(node);
if(viewModel!=null)
Console.WriteLine(viewModel.ToFullString());
Console.ReadLine();
}
}
GenerateViewModel メソッドは、ViewModelGeneration という静的クラスで定義する予定なので、ViewModelGeneration.cs という新しいファイルをプロジェクトに追加します。このメソッドは、入力構文ノード (この例では、ClassDeclarationSyntax オブジェクトの最初のインスタンス) からクラス定義を探して、そのクラスの名前とメンバーを基に新しいビュー モデルを構築します。図 3 に、この例を示します。
図 3 新しい構文ノードの生成
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
namespace RoslynCore
{
public static class ViewModelGeneration
{
public static SyntaxNode GenerateViewModel(SyntaxNode node)
{
// Find the first class in the syntax node
var classNode = node.DescendantNodes()
.OfType<ClassDeclarationSyntax>().FirstOrDefault();
if(classNode!=null)
{
// Get the name of the model class
string modelClassName = classNode.Identifier.Text;
// The name of the ViewModel class
string viewModelClassName = $"{modelClassName}ViewModel";
// Only for demo purposes, pluralizing an object is done by
// simply adding the "s" letter. Consider proper algorithms
string newImplementation =
$@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
get {{ return _{modelClassName}s; }}
set
{{
_{modelClassName}s = value;
OnPropertyChanged(nameof({modelClassName}s));
}}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
var newClassNode =
CSharpSyntaxTree.ParseText(newImplementation).GetRoot()
.DescendantNodes().OfType<ClassDeclarationSyntax>()
.FirstOrDefault();
// Retrieve the parent namespace declaration
if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
// Add the new class to the namespace and adjust the white spaces
var newParentNamespace =
parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
return newParentNamespace;
}
}
else
{
return null;
}
}
}
}
図 3 に示したコードの冒頭では、ビュー モデルをまず文字列として表現しています。ここでは、オリジナルのクラス名を基にオブジェクトとメンバーの名前を指定しやすくする文字列補間を利用しています。このサンプル シナリオでは、単純にオブジェクトとメンバーの名前に “s” を加えて複数形を生成しています。実際のコーディングでは、もっと明確に複数形にするアルゴリズムを使用する必要があります。
図 3 の 2 番目の部分では、コードから CSharpSyntaxTree.ParseText を呼び出して、ソース テキストを SyntaxTree に解析しています。GetRoot は、新しいツリーの SyntaxNode を取得するために呼び出しています。DescendantNodes().OfType<ClassDeclarationSyntax>() により、クラスを表す構文ノードのみを取得し、FirstOrDefault をもつ最初のクラスのみを選択しています。新しいビュー モデル クラスを挿入する親の名前空間を取得するには、構文ノードの最初のクラスを取得すれば十分です。名前空間は、ClassDeclarationSyntax の Parent プロパティを NamespaceDeclarationSyntax にキャストすることで取得できます。クラスは他のクラスの入れ子になる可能性があるため、コードでは、Parent が NamespaceDeclarationSyntax 型であることを検証して、その可能性を最初にチェックします。コードの最後の部分では、ビュー モデル クラスの新しい構文ノードを親名前空間に追加して、これを構文ノードとして返しています。ここで F5 キーを押すと、デバッグ コンソールにコードの生成結果が表示されます (図 4 参照)。
図 4 正しく生成されたビュー モデル クラス
生成されたビュー モデル クラスは、C# コンパイラが操作できる SyntaxNode ノードです。そのため、その後はそれをさらに操作したり、診断情報について分析したり、Emit API を使用してアセンブリにコンパイルしたり、リフレクションを通じて使用することができます。
診断情報の取得
テキストのソースが、文字列、ファイル、ユーザー入力のいずれであっても、Diagnostic API を利用して、エラーや警告などのコードの問題に関する診断情報を取得できます。Diagnostic API ではエラーと警告を取得できるだけでなく、アナライザーやコード リファクタリングも作成できます。前の例に続いて、ビュー モデル クラスの生成を試みる前に、オリジナルのソース コードで構文エラーをチェックすることをお勧めします。これを行うために、SyntaxNode.GetDiagnostics メソッドを呼び出すことができます。このメソッドは、IEnumerable<Microsoft.CodeAnalysis.Diagnostic> オブジェクトがあれば、それを返します。図 5 に、ViewModelGeneration クラスの拡張版を示します。このコードは、GetDiagnostics の呼び出し結果に診断情報が含まれるかどうかをチェックします。診断情報が含まれない場合、コードはビュー モデル クラスを生成します。結果に診断情報のコレクションが含まれる場合、コードは診断ごとに情報を示し、null を返します。Diagnostic クラスは、コードの各問題についての詳細情報を提供します。たとえば、Id プロパティは診断 ID を返します。GetMessage メソッドは完全な診断メッセージを返します。GetLineSpan はソース コード内の診断の位置を返します。Severity プロパティは、「Error (エラー)」、「Warning (警告)」、「Information (情報)」など、診断の重大度を返します。
図 5 Diagnostic API によるコードの問題のチェック
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using System;
namespace RoslynCore
{
public static class ViewModelGeneration
{
public static SyntaxNode GenerateViewModel(SyntaxNode node)
{
// Find the first class in the syntax node
var classNode =
node.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
if(classNode!=null)
{
var codeIssues = node.GetDiagnostics();
if(!codeIssues.Any())
{
// Get the name of the model class
var modelClassName = classNode.Identifier.Text;
// The name of the ViewModel class
var viewModelClassName = $"{modelClassName}ViewModel";
// Only for demo purposes, pluralizing an object is done by
// simply adding the "s" letter. Consider proper algorithms
string newImplementation =
$@"public class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
get {{ return _{modelClassName}s; }}
set
{{
_{modelClassName}s = value;
OnPropertyChanged(nameof({modelClassName}s));
}}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
var newClassNode =
SyntaxFactory.ParseSyntaxTree(newImplementation).GetRoot()
.DescendantNodes().OfType<ClassDeclarationSyntax>()
.FirstOrDefault();
// Retrieve the parent namespace declaration
if(!(classNode.Parent is NamespaceDeclarationSyntax)) return null;
var parentNamespace = (NamespaceDeclarationSyntax)classNode.Parent;
// Add the new class to the namespace
var newParentNamespace =
parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
return newParentNamespace;
}
else
{
foreach(Diagnostic codeIssue in codeIssues)
{
string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()},
Location: {codeIssue.Location.GetLineSpan()},
Severity: {codeIssue.Severity}";
Console.WriteLine(issue);
}
return null;
}
}
else
{
return null;
}
}
}
}
ここで、Program.cs の GenerateSampleViewModel メソッド内の models 変数に含まれるソース テキストに意図的なエラーを組み込んでアプリケーションを実行すると、C# コンパイラが、すべてのコードの問題についての完全な詳細を返すのを確認できます。図 6 に例を示します。
図 6 Diagnostic API を使用したコードの問題の検出
C# コンパイラは、診断情報を含んでいても構文ツリーを生成することに注意してください。これはソース テキストに対して完全に忠実な結果にするだけでなく、新しい構文ノードによりこのような問題を修正する選択肢を開発者に与えます。
コードの実行: Emit API
Emit API により、ソース コードをアセンブリにコンパイルできるようになります。その後、リフレクションにより、コードを呼び出して実行できます。次の例は、コード生成、出力、および診断の検出を組み合わせたものです。EmitDemo.cs という新しいファイルをプロジェクトに追加してから、図 7 に示すコード リストについて検討します。SyntaxTree は、円の面積を計算する静的メソッドを含むヘルパー クラスを定義するソース テキストから生成されます。このクラスから .dll を生成し、半径を引数として渡して CalculateCircleArea メソッドを実行することが目標です。
図 7 Emit API とリフレクションによるコードのコンパイルと実行
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
namespace RoslynCore
{
public static class EmitDemo
{
public static void GenerateAssembly()
{
const string code = @"using System;
using System.IO;
namespace RoslynCore
{
public static class Helper
{
public static double CalculateCircleArea(double radius)
{
return radius * radius * Math.PI;
}
}
}";
var tree = SyntaxFactory.ParseSyntaxTree(code);
string fileName="mylib.dll";
// Detect the file location for the library that defines the object type
var systemRefLocation=typeof(object).GetTypeInfo().Assembly.Location;
// Create a reference to the library
var systemReference = MetadataReference.CreateFromFile(systemRefLocation);
// A single, immutable invocation to the compiler
// to produce a library
var compilation = CSharpCompilation.Create(fileName)
.WithOptions(
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(systemReference)
.AddSyntaxTrees(tree);
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
EmitResult compilationResult = compilation.Emit(path);
if(compilationResult.Success)
{
// Load the assembly
Assembly asm =
AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
// Invoke the RoslynCore.Helper.CalculateCircleArea method passing an argument
double radius = 10;
object result =
asm.GetType("RoslynCore.Helper").GetMethod("CalculateCircleArea").
Invoke(null, new object[] { radius });
Console.WriteLine($"Circle area with radius = {radius} is {result}");
}
else
{
foreach (Diagnostic codeIssue in compilationResult.Diagnostics)
{
string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()},
Location: {codeIssue.Location.GetLineSpan()},
Severity: {codeIssue.Severity}";
Console.WriteLine(issue);
}
}
}
}
}
コードの最初の部分では、変更不可の C# コンパイラ呼び出しを 1 つ表す、新しいコンパイルを作成します。CSharpCompilation オブジェクトにより、Create メソッドによるアセンブリの作成、WithOptions による生成する出力の種類 (今回は DynamicallyLinkedLibrary) の指定が可能になります。AddReferences を使用して、コードで必要な参照を追加します。そのためには、コードに必要なのと同じ参照を含む型を提供しなければなりません。今回の場合は、オブジェクト型が使用するのと同じ参照が必要です。GetTypeInfo().Assembly.Location により参照用のアセンブリ名を取得後、MetadataReference.CreateFromFile がコンパイル内部のアセンブリへの参照を作成します。最後に、AddSyntaxTrees を使ってコンパイルに構文ツリーを追加します。
コードの 2 番目の部分では、CSharpCompilation.Emit を呼び出して、バイナリの生成を試みて、EmitResult 型のオブジェクトを返します。この最後の部分に、非常に興味深い点があります。 この型のオブジェクトは、コンパイルが正常終了したかどうかを示すブール型の Success プロパティと、コンパイルが失敗した理由を理解するのに役立つ Diagnostic オブジェクトの変更不可の配列を返す Diagnostics というプロパティを公開します。図 7 には、コンパイルが失敗する場合に Diagnostics プロパティを反復する方法を示しています。出力アセンブリは .NET 標準ライブラリになるため、ソース テキストのコンパイルは、Roslyn によって解析されたコードが .NET 標準に含まれる API を利用する場合のみ成功します。
では、コンパイルが正常終了すると何が起きるか見てみましょう。今回冒頭でインポートした同じ名前の NuGet パッケージに含まれる System.Runtime.Loader 名前空間は、AssemblyLoadContext というシングルトン クラスを公開します。このクラスは LoadFromAssemblyPath というメソッドを公開します。このメソッドは Assembly クラスのインスタンスを返します。このクラスにより、リフレクションを使用して Helper クラスへの参照を最初に取得した後、CalculateCircleArea メソッドへの参照を取得できます。このメソッドは、半径パラメーターの値を渡して呼び出すことができます。CalculateCircleArea は静的メソッドであるため、MethodInfo.Invoke メソッドは最初の引数として null を受け取ります。そのため、型のインスタンスを渡す必要はありません。Program.cs の Main から GenerateAssembly メソッドを呼び出すと、この動作の結果を確認できます。図 8 に示すように、計算結果がデバッグ コンソールに表示されます。
図 8 Roslyn が生成したコードのリフレクションによる呼び出しの結果
想像どおり、OS を問わず C# コードを生成、分析、および実行できるため、.NET Core での Emit API とリフレクションの組み合わせは非常に強力かつ柔軟性があります。実際、今回紹介したすべての例は、Windows だけでなく、macOS やほとんどの Linux ディストリビューションでも確実に動作します。また、Roslyn Scripting API を使用してライブラリからコードを呼び出すことができるため、リフレクションに縛られることはありません。
まとめ
.NET Core により、複数の OS やデバイスで実行されるクロスプラットフォーム アプリケーションを C# コードで作成できます。これは、コンパイラ自体がクロスプラットフォーム対応であるためです。.NET コンパイラ プラットフォームの Roslyn は、.NET Core の C# コンパイラを強化するため、開発者はリッチなコード分析 API を利用して、コードの生成、分析、およびコンパイルが可能になります。このことは、Windows、macOS、Linux を問わず、実行時にコードの生成と実行することによるタスクの自動化、ソース テキストのコードの問題についてのテキストの分析、大量のアクティビティのソース コードでの実行が可能になることを意味します。
Alessandro Del Sole は 2008 年から Microsoft MVP の一員です。彼は年間 MVP を 5 度受賞し、Visual Studio による .NET 開発に関する、書籍、電子ブック、説明ビデオ、記事を手がけてきました。彼は主に .NET およびモバイル アプリの開発、教育、コンサルティングに取り組む、シニア .NET 開発者として働いています。Twitter は、@progalex (英語) からフォローできます。
この記事のレビューに協力してくれたマイクロソフト技術スタッフの Dustin Campbell に心より感謝します。
Dustin Campbell は Microsoft の主席エンジニアであり、C# 言語設計チームの一員です。彼は Roslyn にその誕生以来から取り組み、現在は Visual Studio Code 用 C# 拡張機能を担当しています。