dotnet-script によるC#スクリプトの実行/デバッグ

# 概要

dotnet-script はC#で書かれたコードをスクリプトのように実行するツールです。
利用方法としてテキストのC#コードファイルをスクリプトのようにそのまま実行する形で、主にコンソールアプリケーションのような処理を行わせることに利用できるため、シェルスクリプトやバッチファイルのようなものに変わる道具として利用できます。

dotnet-script は NuGet パッケージの利用に対応しており、スクリプト内に参照するパッケージ名を記述するだけで利用することができます。.NETの強力な下地に加えてパッケージで提供されている機能を容易に利用できるため非常に広範な用途に利用可能です。

## 簡単な例

できることが広範なので例を出すのが逆に難しいですが、先述したパッケージ参照とシェルスクリプト的な面を含むものとして、あるディレクトリ配下のリポジトリすべてにgitコマンドでfetchするような例を記述します。
コマンド呼び出しを容易に記述できるProcessXと、色付けエスケープシーケンス出力を容易に記述できるKokubanを、NuGetからパッケージ参照して利用しています。

```c#:git-fetch.csx
#r "nuget: ProcessX, 1.5.5"
#r "nuget: Kokuban, 0.2.0"
using Zx;
using Kokuban;

var searchDir = new DirectoryInfo(@"/path/to/target/dir");
foreach (var repoDir in searchDir.EnumerateDirectories(".git", SearchOption.AllDirectories))
{
    var workDir = repoDir.Parent;
    Console.WriteLine(Chalk.Green[$"Repo: {workDir.FullName}"]);
    var output = await $"git -C {workDir.FullName} fetch";
}
```

## 実行環境の準備

利用のためにインストールする必要があるのものは以下の2つです。

1. .NET SDK のインストール
    - スクリプトは実行時にコンパイルされるため Runtime ではなく SDK が必要です。
    - https://dotnet.microsoft.com/ja-jp/download
2. dotnet-script のインストール
    - .NET がインストール済みの状態で以下を実行するとインストールできます。
        - `dotnet tool install -g dotnet-script`
    - これは特権ユーザではなく通常ユーザ権限で実行する必要があります。
    - インストール済みの場合、以下でバージョン更新を行うこともできます。
        - `dotnet tool update -g dotnet-script`

### スクリプトの実行

dotnet-scriptの実体は`dotnet-script`コマンドですが、dotnetコマンドのサブコマンドのように以下のようにしてスクリプトを実行できます。
```
dotnet script ./my-script.csx
```

テキストソースから実行開始できるためC#スクリプトと呼ばれますが、実際のところ実行時にはスクリプトファイルを元にNuGetパッケージの取得やコンパイルが行われ、実行バイナリが作られて実行されます。
そのため、スクリプトの初回実行時には実行開始してからある程度時間がかかります。
1度ビルドされるとキャッシュが作られるため、2度目以降はそれほど時間がかかりません。

#### Windows での関連付け

これは Windows のみですが、以下のコマンドによって関連付けハンドラを登録することができます。
```
dotnet script register
```

直ちに関連付けされるわけではなく、ファイルを開くアプリケーションの選択に「.NET Host」が追加されます。たとえば `.csx` などの拡張子に対して関連付けると便利かと思います。

## デバッグ環境の準備

スクリプトは Visual Studio Code を利用して IntelliSense の効いた編集やデバッグを行うことが可能です。
以下では 2024/01/21 時点の状況で、.NET 8 と dotnet-script v1.5.0 を利用する場合の例を記載します。

-  Visual Studio Code をインストールします。
-  Visual Studio Code に [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) 拡張をインストールします。
    - C#拡張は v2.15.30 以降でなければなりません。
		- このバージョンで OmniSharp v1.39.11 が利用されます。
		- これによって dotnet-script v1.5.0 がサポートされます。
- C#拡張が OmniSharp を利用するように構成します。
	- 2024/01/21 時点では OmniSharp の言語サーバでしか dotnet-script をサポートしていません。
	- 拡張機能の説明ページに記載がありますが、Omnisharpが利用されるようにするには以下の両方が必要です。
		- C#拡張機能の `dotnet.server.useOmnisharp` 設定(Dotnet › Server: Use Omnisharp)を ON にします。
		- [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) 拡張がインストールされている場合は無効化またはアンインストールします。
- C#スクリプトファイルの拡張子は `.csx` にします。
	- dotnet-script 用のファイルとしては、これ以外の拡張子はサポートされていません。
- スクリプトファイルのあるディレクトリで `dotnet script init` を実行しておきます。
	- これは `omnisharp.json` と `.cscode/launch.json` を用意するためです。
	- 必要な内容がわかっていれば init コマンドを使わずに用意してもかまいません。
- 上記が済んだ状態で Visual Studio Code によってスクリプトディレクトリを開きます。

### デバッグ環境の注意点

C#スクリプト編集で IntelliSense が正常に機能するためには、Visual Studio CodeとC#拡張とOmniSharpと.NET SDKとdotnet-scriptのバージョンの組み合わせが関係してきます。
いずれかの環境がバージョンアップした場合などには上手く機能しなくなる場合があります。
そのような場合に回避策を講じるために、以下のことを知っておくとよいかもしれません。

- OmniSharp によってサポートされる dotnet-script のバージョンが直接的に決まる。
    - OmniSharp が csx ファイルを処理するために dotnet-script を同梱・利用しています。  
      このバージョンが利用環境と合っていないとうまく機能しない場合があります。
    - もしC#拡張の更新が間に合っておらずOmniSharpが古いことで問題が生じている場合、設定項目 `dotnet.server.path` (Dotnet › Server: Path)に `latest` を指定すると最新のビルドが取得・使用されるため問題を回避できる場合があります。
        - 参考：[dotnet-script 1.5 is out with .NET 8.0 support](https://www.strathweb.com/2023/11/dotnet-script-1-5-is-out-with-net-8-0-support/)
- OmniSharp が利用するSDKは自動的に選択されます。
    - C#拡張にSDKが同梱されていますが、システムにインストールされたSDKがそれよりも新しい場合はそちらが利用されるはずです。
    - これによってdotnet-scriptのサポートするSDKとOmniSharpの使うSDKのバージョンが合わず、IntelliSenseが効かなくなる場合ことあります。
    - 出力ウィンドウでログ種別 `Omnisharp Log` を参照すると、ログからどのSDKが選択されているかを確認できます。

## その他問題が生ずる可能性がある点

### キャッシュと Windows ストレージセンサとの相性

Windows 上ではスクリプトのビルドキャッシュがデフォルトでユーザのTempディレクトリ配下に保存されます。
(通常は `%TEMP%\dotnet-script` 配下にキャッシュが存在するはずです。)
キャッシュにはスクリプトのビルド結果DLLと参照パッケージのDLL類などが格納されますが、しストレージセンサをONにしている場合はタイムスタンプの古い一部のDLLだけが削除されてしまい「スクリプトビルドキャッシュはあるが一部DLLが無くて実行できない」という状況が生じます。
そのような場合はキャッシュを一旦削除する必要があります。
あるいは環境変数 `DOTNET_SCRIPT_CACHE_LOCATION` を設定し、ストレージセンサの適用されない場所にキャッシュを作らせる対処もあります。

### アセンブリの競合

スクリプトが実行される際`.csx`からビルドされたDLL(アセンブリ)は、`dotnet-script` の実行ファイルによってロードして実行されます。
この仕組みのため、dotnet-script本体が使うアセンブリとスクリプトから使うアセンブリの間で競合が生じて実行できない場合があります。
たとえばdotnet-script本体はMicrosoft.Extensionsを利用しており、スクリプト内でそれらを使うパッケージを利用しようとした場合には、本体でロード済みのものと同名アセンブリでバージョンだけ違うためロードできないということがあります。
例えばよく使いたくなるものとしてはEntityFrameworkなどがこれに引っかかったりします。

この問題を回避するためにコマンドラインオプション `--isolated-load-context` が用意されています。
このオプションを利用すると、スクリプトをビルドしたDLLは AssemblyLoadContext によって分離されたコンテキストにロードされます。

以下のように指定して実行することで分離コンテキストを有効にした実行を行えます。
```
dotnet script --isolated-load-context ./my-script.csx
```