【C#】Roslyn for Scriptingについて


# はじめに
Roslyn for Scriptingについていろいろ触ってみたのでまとめました。

# 動作環境
Visual Studio 2017 Community
.NET Framework 4.6.2
Microsoft.CodeAnalysis.CSharp.Scripting 2.1.0

# 使い方
[nuget](https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp.Scripting)でパッケージをインストール出来ます。

```text
Install-Package Microsoft.CodeAnalysis.CSharp.Scripting
```

パッケージインストール後、Roslyn for ScriptingのAPIが使用できるようになります。
以下、サンプルコードを示します。

```csharp
using System;
using System.IO;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

namespace Sample
{
	class Program
	{
		static void Main( string[] args )
		{
			try {
				var script = CSharpScript.Create( File.ReadAllText( "hello.csx" ) ) ;
				script.RunAsync();
			}
			catch( CompilationErrorException ex ) {
				Console.WriteLine( "[Compile Error]" );
				Console.WriteLine( ex.Message );
			}
			catch( Exception ex ) {
				Console.WriteLine( ex );
			}
		}
	}
}
```

```csharp
//hello.csx
using System;

Console.WriteLine( "Hello Roslyn for Scripting!" );
```

その他のサンプルコードについてはRoslynのgithubのwikiにあります。
https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples

また、日本語でのサンプルならば以下のサイトが一番まとまっていると思います。
http://wiki.clockahead.com/index.php?Coding/.NET/Roslyn/Scripting

# トラブルシューティング
今までハマったことについて。同じ現象が起きた方の助けになれば幸いです。

## nugetでのインストールに失敗する(エラーが出る)
ターゲットフレームワークが.NET Framework 4.6(.NET Core 1.1)以前のバージョンになっている可能性があります。プロジェクトのプロパティからターゲットフレームワークを変更出来ます。

## 初回だけスクリプト実行まで時間が掛かる
初回だけRunAsync呼び出しからスクリプト実行まで時間が掛かります。
一応issueとして挙がっているようですが、現在(V2.1.0)はまだ修正されていないようです。

https://github.com/dotnet/roslyn/issues/16897

## 非同期のはずなのに同期処理になる
RunAsync()は非同期メソッドでawaitしていても同期になり、GUIの場合はUIが固まる事があります。
スクリプトで同期処理を行っている事が原因です。非同期処理にすると非同期になります。

```csharp
//同期の場合
using System;
using System.Threading;

Thread.Sleep( 1000 );
Console.WriteLine( "1ms elapsed" );
```

```csharp
//非同期の場合
using System;
using System.Threading.Tasks;

await Task.Delay( 1000 );
await Task.Run( () => Console.WriteLine( "1ms elapsed" ) );
```

## RunAsyncのキャンセルが出来ない
RunAsyncには他のTaskと同様にcancelationTokenを渡すオーバーロードが存在しますが、現在スクリプト実行中にキャンセルさせる事が出来ません(スクリプト実行前にキャンセルした場合はOperationCanceledExceptionが発生してキャンセルされます)。
以下の関数を呼び出し、関数内のCancelメソッド呼び出し後もキャンセルされずにスクリプトを実行し続けます。

[コード](https://github.com/dotnet/roslyn/blob/master/src/Scripting/Core/ScriptExecutionState.cs#L86)を読んでみると内部でThrowIfCancellationRequestedメソッドを呼び出しているようにも見えるのですが・・

```csharp
public async static Task RunTaskAsync()
{
	try {
		var script = CSharpScript.Create( File.ReadAllText( "delay.csx" ) );
		var cancelTokenSrc = new CancellationTokenSource();

		var cancelTask = Task.Run( () =>
		{
			Thread.Sleep( 4000 );   //スクリプト実行まで時間が掛かるので適当にwait
			cancelTokenSrc.Cancel();
			Console.WriteLine( "Cancel executed" );
		} );

		await Task.WhenAll( script.RunAsync( cancellationToken: cancelTokenSrc.Token ), cancelTask );
	}
	catch( CompilationErrorException ex ) {
		Console.WriteLine( "[Compile Error]" );
		Console.WriteLine( ex.Message );
	}
	catch( TaskCanceledException ) {
		Console.WriteLine( "Task is canceled" );
	}
	catch( OperationCanceledException ) {
		Console.WriteLine( "Operation is canceled" );
	}
	catch( Exception ex ) {
		Console.WriteLine( ex );
	}
}
```

```csharp
//delay.csx
using System;
using System.Threading.Tasks;

for( int i = 0; i < 10; i++ ){
	await Task.Delay( 1000 );
	await Task.Run( () => Console.WriteLine( $"{( i + 1 )*1000}ms elapsed" ) );
}
```

#### 実行結果
```text
1000ms elapsed
2000ms elapsed
Cancel executed
3000ms elapsed
4000ms elapsed
5000ms elapsed
6000ms elapsed
7000ms elapsed
8000ms elapsed
9000ms elapsed
10000ms elapsed
```

対策として、スクリプト側でキャンセルするかスクリプト内で呼び出すメソッド内でキャンセルする事でTaskCanceledExceptionをthrowさせる事が出来ます。

### スクリプト内でキャンセルする場合
```csharp
public async static Task RunTaskAsync()
{
	try {
		var script = CSharpScript.Create( File.ReadAllText( "delay.csx" ) );
		await script.RunAsync();
	}
	catch( CompilationErrorException ex ) {
		Console.WriteLine( "[Compile Error]" );
		Console.WriteLine( ex.Message );
	}
	catch( TaskCanceledException ) {
		Console.WriteLine( "Task is canceled" );
	}
	catch( OperationCanceledException ) {
		Console.WriteLine( "Operation is canceled" );
	}
	catch( Exception ex ) {
		Console.WriteLine( ex );
	}
}
```
```csharp
//delay.csx
using System;
using System.Threading;
using System.Threading.Tasks;

var cancelTokenSrc = new CancellationTokenSource();

for( int i = 0; i < 10; i++ ){
	await Task.Delay( 1000, cancelTokenSrc.Token );
	await Task.Run( () => Console.WriteLine( $"{( i + 1 )*1000}ms elapsed" ) );

	if( i == 3 ){
		cancelTokenSrc.Cancel();
		Console.WriteLine( "Cancel executed" );
	}
}
```

#### 実行結果
```text
1000ms elapsed
2000ms elapsed
3000ms elapsed
4000ms elapsed
Cancel executed
Task is canceled
```

### スクリプト内で呼び出すメソッド内でキャンセルする場合

```csharp
public async static Task RunTaskAsync()
{
	try {
		var script = CSharpScript.Create( File.ReadAllText( "delay.csx" ), globalsType: typeof( CancelableAPI ) );
		var api = new CancelableAPI();

		var cancelTask = Task.Run( () =>
		{
			Thread.Sleep( 4000 );   //スクリプト実行まで時間が掛かるので適当にwait
			api.Cancel();
			Console.WriteLine( "Cancel executed" );
		} );

		await Task.WhenAll( script.RunAsync( api ), cancelTask );
	}
	catch( CompilationErrorException ex ) {
		Console.WriteLine( "[Compile Error]" );
		Console.WriteLine( ex.Message );
	}
	catch( TaskCanceledException ) {
		Console.WriteLine( "Task is canceled" );
	}
	catch( OperationCanceledException ) {
		Console.WriteLine( "Operation is canceled" );
	}
	catch( Exception ex ) {
		Console.WriteLine( ex );
	}
}
```

```csharp
public class CancelableAPI
{
	private CancellationTokenSource cancelTokenSrc;

	public CancelableAPI()
	{
		cancelTokenSrc = new CancellationTokenSource();
	}

	public async Task Delay( int milliseconds )
	{
		await Task.Delay( milliseconds, cancelTokenSrc.Token );
	}

	public void Cancel()
	{
		cancelTokenSrc?.Cancel();
	}
}
```

```csharp
//delay.csx
using System;
using System.Threading.Tasks;

for( int i = 0; i < 10; i++ ){
	await Delay( 1000 );
	await Task.Run( () => Console.WriteLine( $"{( i + 1 )*1000}ms elapsed" ) );
}
```

#### 実行結果
```text
1000ms elapsed
Cancel executed
Task is canceled
```

# おわりに
Roslyn for Scriptingについて記載しました。

Visual Basicの方についても[現在開発中](https://github.com/dotnet/roslyn/issues/6897)みたいですので今後、Visual Basic版のRoslyn for Scriptingも使えるようになるかもしれません。