VSCode+C#で依存パッケージも含めてデバッグも行う

tl;dr
---------


vscodeのlaunch.jsonに以下の設定をしましょう。

```jsonc
            "justMyCode": false, // 自分のコード以外
            "symbolOptions": {
                "searchPaths": [],
                "searchMicrosoftSymbolServer": true, // ここをtrue
                "searchNuGetOrgSymbolServer": false, // その他の外部ライブラリも見る場合はこちらも
            }
```

その他の設定はドキュメントを見るとよいでしょう。

https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md


参考
-------------

最初はこのあたりを見つけました。

https://techinfoofmicrosofttech.osscons.jp/index.php?NuGet%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0#zd224c6d

https://docs.microsoft.com/ja-jp/dotnet/core/diagnostics/symbols

その後、"symbol"等で検索をして以下の記事を見つけたおかげでVSCodeでも.NET Coreのデバッグができることを確信できました。

https://dev.to/notmyself/debugging-net-core-via-symbol-server-on-osx-in-vscode-3pge

この方法でもできましたが、手順が煩雑だったので簡単な方法があるはずだとomnisharp-vscodeの設定を眺めていたら、便利オプションがあったため、ついでに記事にしたという流れです。

https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md



背景
------------

最近はVSCodeと.NET Core 3.1で開発をしています。（.NET 6移行しないと・・・）

ある日、ASP.NET MVCでTempDataAttributeをEnumのプロパティに対して使うと値が削除されないまま残るという事象にぶつかりました。
動かしているうちにenumを使うと起きるという事がわかったものの、「enum特有の問題なのか？」とか「逆に他のプリミティブな型はどうしているんだ？」とか、なぜそうなるのか気になり、ASP.NET MVCのコードをさらに追いかけたいと思いました。

しかし、ソースコードだけでは追いかけるのがしんどかったので、デバッガを使って変数やスタックトレースを見ながら追いかけられないかと思い、調べました。（そうして本来やるべきことから脱線していく）


前提
-----------

- Windows 10
- .NET Core 3.1.x
- VSCode 
    - 機能拡張: C#

手順
----------

### シンボルファイルを取得するよう設定する

VSCodeのデバッグ実行の設定が書かれている`.vscode/launch.json`を開き、.NETプロジェクトの設定を修正します。

```jsonc
            "justMyCode": false, // 自分のコード以外
            "symbolOptions": {
                "searchPaths": [],
                "searchMicrosoftSymbolServer": true, // ここをtrue
                "searchNuGetOrgSymbolServer": false, // その他の外部ライブラリも見る場合はこちらも
            }
```

まず`justMyCode`は`false`にしてください。でないと、`シンボルの読み込みをスキップしました。モジュールは最適化されていて、デバッグ オプションの [マイ コードのみ] 設定が有効になっています。`と出てシンボルの読み込みが機能しませんし、コールスタックも自身のコードだけになります。ただ、`justMyCode`を`false`にするだけで、コールスタックとその変数をみることはできます。

次に`symbolOptions`の`searchMicrosoftSymbolServer`を有効にします。ここがあるとシンボルファイルがないときにダウンロードしてくれます。`searchNuGetOrgSymbolServer`は必要に応じて設定してください。

これだけです。

### デバッグ実行する

デバッグ実行します。

初回はシンボルのダウンロードを行うので時間がかかりますが、デバッグコンソール上から機能していることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/503df7a7-4627-3aa8-c5d5-e4a0add3919e.png)

たまにダウンロードが失敗するときがありシンボルが読み込めないときがあります。その時は再度デバッグ実行してみてください。おそらく短期間にアクセスが集中すると起きるものと思います。

なお、ダウンロードしたものはデフォルトで`%USERPROFILE%\AppData\Local\Temp\SymbolCache`にキャッシュされ、launch.jsonの設定で変更できます。


後はいつも通りデバッグしましょう。
コールスタックをクリックするとソースコードも出ますし、ブレークポイントを置くことステップ実行もできます。

ただ、次の画像のように、行数が合わないことがありブレークポイントが効かないこともしばしばありますが、Javaのころもそうだったのであまり困っていません。変数と周辺の概ねのコードがわかるだけで十分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/265758ee-3bee-4424-fc22-edc6c62c8b9d.png)


PDBやらシンボルやらデバッグの裏の仕組みはわかってないですが、簡単にできるのはありがたいですね。


おまけ
-------------

### `値を取得することはできません。最適化されている可能性があります` と出て変数の値がわからない

事象としてはあるコールスタックを見に行くと以下のような感じになることがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/a983860c-ec6c-e94a-e64c-d502d58ede68.png)

環境変数 `COMPlus_ReadyToRun`を`0`に指定すると解消される場合があります。

```
      "env": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "COMPlus_ReadyToRun": "0" // <- 追加
      },
```

ドキュメントを読むと`"suppressJITOptimizations": true`についても触れられていますが、私が作業していた範囲ではデフォルト(false)でも問題ありませんでした。

このあたりはomnisharpのドキュメントをご覧ください。

https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#suppress-jit-optimizations


### きっかけだったTempDataAttributeが付いたenumプロパティが残り続ける事象はどうなったのか？

事象自体は既知ですが対応はされていないようです。

https://github.com/dotnet/aspnetcore/issues/30645#issuecomment-790979076

比較に使う値がJsonデシリアライズしたものを保持しておいて、処理後のプロパティ値と比較して変化していたら、TempDataに足す、という処理をしています。Jsonデシリアライズした段階ではenumではなくintとして扱うので、enumとintの比較がうまくいかない（`1 != (Enum)1`になる）ので、常に変更扱いにされてずっとTempDataに残り続ける動きをしているようです。

そもそも仕組み上、プロパティが変更されたことを値の不一致で確認するのは良くなくて、同じ値が設定されるケースでは変更なしとなってしまう気がします。そういったこともあり、私ではどうあるべきかはよくわからない、という感じでした（フィルタで行うので同値でも変更と検知するのは難しい気はします）

なお、TempDataAttributeでバインドするのではなく、TempData(TempDataDictionary)を直接いじる場合は、フィルタを使わなかったりIDictionaryをラップしたクラスを通していじるので変更が記録されているので事象に引っかかることなく動く。（取り出すときにenumへのキャストは必要だけど）