【VSCode, C#, AWS Lambda】C#で作成したLambda Functionをローカルで動作確認する

# この記事について
C#で作ったLambda Functionをローカルで実行する方法を記載します。
Lambda Functionの動作確認の方法として、毎回AWSにアップロードしてテストする方法もありますが、不具合を見つけられるのがアップロード後では不便な点も多くなってしまいます。ローカルで動作確認してからアップロードできると良さそうですね。
そこでこの記事では、Lambda Functionをローカルでビルド・実行する方法を記載します。

# 対象読者
VSCodeを使用してLambda Functionの開発を行っている方。
※今回C#を使用していますが、VSCodeの設定は他言語でも使用できる考え方のため、適宜読み替えてもらえればと思います。

# 環境構築
|ツール|バージョン|
|---|---|
|Mac||
|VSCode|1.79.0|
|[dotnet](https://dotnet.microsoft.com/ja-jp/download/dotnet/6.0)|6.0.408（LTS）|

以下コマンドでAWS Lamdaの.NET用開発ツールをインストールします。
```
dotnet tool install -g Amazon.Lambda.Tools
```
VSCodeでデバッグ起動した際に[The AWS .NET Mock Lambda Test Tool](https://github.com/aws/aws-lambda-dotnet/tree/master/Tools/LambdaTestTool)を起動できるように、Test Toolをインストールします。
```
dotnet tool install -g Amazon.Lambda.TestTool-6.0
```
今回、dotnetのバージョンは6系を使用するためAmazon.Lambda.TestTool-6.0をインストールしています。**dotnetの他バージョンを使用する際は、バージョンに応じてインストールするTest Toolが変わります。**
参考：[The AWS .NET Mock Lambda Test Tool - Versions of the tool](https://github.com/aws/aws-lambda-dotnet/tree/master/Tools/LambdaTestTool#versions-of-the-tool)

# プロジェクト構成
最終的に以下の構成となるように、ソリューションやプロジェクトを作成していきます。
Lambda Functionから実行されるプログラムは他でも使いまわせるように別プロジェクトしたい、ということも考えられるため、「Lambda Functionのプロジェクト（SampleFunction）」と「Lambda Functionから実行されるプロジェクト（DependentLibrary）」の2つのプロジェクトに分けることにします。

※今回はC#・dotnetを使用しているため「ソリューション」や「プロジェクト」といったものが必要になりますが、他言語を使用している場合はこれらの作成は不要となります。
```
% tree LambdaSampl
LambdaSample
├── DependentLibrary
│   ├── Class1.cs
│   ├── DependentLibrary.csproj
├── LambdaSample.sln
└── SampleFunction
    ├── src
    │   └── SampleFunction
    │       ├── Function.cs
    │       ├── Readme.md
    │       ├── SampleFunction.csproj
    │       └── aws-lambda-tools-defaults.json
    └── test
        └── SampleFunction.Tests
            ├── FunctionTest.cs
            └── SampleFunction.Tests.csproj
```
この構成を作成する手順を以下で説明します。

## ソリューションの作成
後で2つのプロジェクトをまとめるために、はじめにソリューションを作成します。
```
% mkdir LambdaSample
% cd LambdaSample
% dotnet new sln
テンプレート "ソリューション ファイル" が正常に作成されました。
```

## プロジェクトの作成
今回は以下の2つのプロジェクトを作成します。
* Lambda Functionから実行されるクラスライブラリのプロジェクト（DependentLibrary）
* Lambda Function（SampleFunction）

まず、Lambda Functionから実行されるクラスライブラリのプロジェクトを作成します。
**Lambda Functionから実行されるプロジェクトは`classlib`指定（クラスライブラリ）で作成してください。**
`console`指定（コンソールアプリ）だと、後にThe AWS .NET Mock Lambda Test Toolの起動が失敗してします。
参考：[Unable to get the LambdaTestTool to find the assembly that needs to be debugged when using multiple projects. #1235](https://github.com/aws/aws-lambda-dotnet/issues/1235#issuecomment-1181045785)
> added Project.Api class library project
```
% dotnet new classlib --name DependentLibrary
テンプレート "クラス ライブラリ" が正常に作成されました。

作成後の操作を処理しています...
/LambdaSample/DependentLibrary/DependentLibrary.csproj で ' dotnet restore ' を実行しています...
  復元対象のプロジェクトを決定しています...
  /LambdaSample/DependentLibrary/DependentLibrary.csproj を復元しました (30 ms)。
正常に復元されました。
```
ソリューションファイルのあるディレクトリで`dotnet build`コマンドを実行した際、このプロジェクトがビルドされるように、ソリューションにこのプロジェクトを追加します。
```
% dotnet sln add DependentLibrary/DependentLibrary.csproj
プロジェクト `DependentLibrary/DependentLibrary.csproj` をソリューションに追加しました
```

次に、Lambda Functionのプロジェクトを作成します。
`dotnet new lambda.EmptyFunction`コマンドについての参考：[.NET Core CLI - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/csharp-package-cli.html)
```
% dotnet new lambda.EmptyFunction --name SampleFunction
テンプレート "Lambda Empty Function" が正常に作成されました。
```
同様に、Lambda Functionのプロジェクトもソリューションに追加します。
Lambda Functionのプロジェクトファイルパスは`<プロジェクト名>/src/<プロジェクト名>/<プロジェクト名>.csproj`となる点に注意してください。
```
% dotnet sln add SampleFunction/src/SampleFunction/SampleFunction.csproj
プロジェクト `SampleFunction/src/SampleFunction/SampleFunction.csproj` をソリューションに追加しました
```
また、SampleFunctionプロジェクトからDependentLibraryプロジェクトを参照できるように以下を実行します。
参考：[Depend on local C# project](https://stackoverflow.com/questions/62465855/depend-on-local-c-sharp-project)
```
% dotnet add SampleFunction/src/SampleFunction/SampleFunction.csproj reference DependentLibrary/DependentLibrary.csproj
参照 `..\..\..\DependentLibrary\DependentLibrary.csproj` がプロジェクトに追加されました。
```

# VSCodeの設定ファイルを作成
VSCodeでは「デバッグ起動した際の設定」や「ビルドなどのタスク設定」をJSONファイルで定義することができます。
参考：[Visual Studio CodeでC#コードをデバッグするための構成ファイル設定の基本](https://atmarkit.itmedia.co.jp/ait/articles/1709/01/news034_2.html)
今回「C#で作成したプログラムをビルド」→「ビルドしたプログラムをデバッグ起動」するため、以下2ファイルを作成します。
* .vscode/tasks.json...ビルドタスクを定義するための設定ファイル
* .vscode/launch.json...デバッグ起動時の条件などを定義するための設定ファイル

## ビルドタスクの設定追加（tasks.jsonの作成）
.vscode/tasks.jsonを作成して、以下を記述します。
```json:.vscode/tasks.json
{
    "version": "2.0.0",
    "command": "dotnet",
    "args": [],
    "tasks": [
        {
            "label": "build packages",
            "args": [
                "build",
                "${workspaceFolder}"
            ],
            "type": "shell",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher":"$msCompile"
        }
    ]
}
```
* ビルドには`dotnet`コマンドを使用するため、`command`は`"dotnet"`を指定する
* tasksの中について
    * `label`...taskを任意の名称で設定する
    * `arg`...dotnetコマンド実行時のコマンドライン引数を指定する
        * ソリューションに2つのプロジェクトが紐づいているため、ソリューションがあるディレクトリで`dotnet build`を実行するとプロジェクトが2つともビルドされる
        * 以下のように指定すると、`dotnet build <workspaceディレクトリのパス>`のコマンドが実行される
          ```　json
          "args": [
            "build",
            "${workspaceFolder}"
          ],
          ```

## デバッグ起動の設定追加（launch.jsonの作成）
.vscode/launch.jsonを作成して、以下を記述します。
launch.jsonファイルを作成すると、「構成を追加」ボタンが表示されるため、そのボタンをクリックして「.NET Attatch to local .NET Core Console App」を選択するとJSONの中身が自動で追加されます。
```json-doc:.vscode/launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": ".NET Core Launch (console)",
            "type": "coreclr",
            "request": "launch",
            "preLaunchTask": "build packages", // <=変更
            "program": "${env:HOME}/.dotnet/tools/dotnet-lambda-test-tool-6.0", // <=変更
            "args": [],
            "cwd": "${workspaceFolder}/SampleFunction/src/SampleFunction/bin/Debug/net6.0", // <=変更
            "console": "internalConsole",
            "stopAtEntry": false,
            "internalConsoleOptions": "openOnSessionStart"
        }

    ]
}
```
* preLaunchTask...tasks.jsonで設定したビルドタスク（のlabelに記述した名称）を記述する
* program...[The AWS .NET Mock Lambda Test Tool](https://github.com/aws/aws-lambda-dotnet/tree/master/Tools/LambdaTestTool)のインストール先を指定する（デフォルトで`"${env:HOME}/.dotnet/tools/dotnet-lambda-test-tool-6.0"`がインストール先となる）
    * Windowsを使用している場合は、`${env:HOME}`を`${env:USERPROFILE}`に変更する必要がある

# Lambda Test Toolの起動
ローカルでLambda Functionを実行してみましょう。
VSCode上でF5キーを押すか、画像赤枠からデバッグ起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234275/aafc9474-d19d-0a8a-129b-5e5616b45ee7.png)
（この際、`SampleFunction/src/SampleFunction/aws-lambda-tools-defaults.json`のJSONの内容によって起動する関数の指定など、起動条件を設定することができます。）

ブラウザで`http://localhost:5050/`にアクセスして以下の画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234275/719354a8-962d-2ff4-c84f-add8de317a0c.png)
「Execute Function」ボタンをクリックしてLambda Functionをローカルで実行することができます。
ブレークポイントをVSCode上で張っておくと、「Execute Function」ボタンをクリックした時にブレークポイントで処理を止めることもできます。

今回、2つのプロジェクトを作成しているので、Lambda Functionのプロジェクトからもう1つのプロジェクトの処理を使用してみます。
（Lambda Functionから参照されるプロジェクトの処理）
```cs:DependentLibrary/Class1.cs
namespace DependentLibrary;
public class Class1
{
    public static void Execute()
    {
        Console.WriteLine("DependentLibrary.Class1が実行されました");
    }
}
```

（Lambda Functionのプロジェクトの処理）
```cs:SampleFunction/src/SampleFunction/Function.cs
using Amazon.Lambda.Core;
using DependentLibrary; // 追加

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace SampleFunction;

public class Function
{
    
    /// <summary>
    /// A simple function that takes a string and does a ToUpper
    /// </summary>
    /// <param name="input"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public string FunctionHandler(string input, ILambdaContext context)
    {
        Console.WriteLine("DependentLibrary.Class1を実行します"); // 追加
        Class1.Execute(); // 追加
        return input.ToUpper();
    }
}
```
以下3行を新しく追加しています。
```cs
using DependentLibrary; // 追加
Console.WriteLine("DependentLibrary.Class1を実行します"); // 追加
Class1.Execute(); // 追加
```

デバッグ起動して画面上で「Execute Function」ボタンをクリックすると、「Log Output:」に`DependentLibrary.Class1が実行されました`が表示されるため、参照先のプロジェクトの処理が実行されたことが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234275/1eedb622-8799-cd5e-aab7-f367123ec73f.png)

# 参考
* [Unable to get the LambdaTestTool to find the assembly that needs to be debugged when using multiple projects. #1235](https://github.com/aws/aws-lambda-dotnet/issues/1235#issuecomment-1181045785)
* [.NET Core CLI - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/csharp-package-cli.html)
* [Depend on local C# project](https://stackoverflow.com/questions/62465855/depend-on-local-c-sharp-project)
* [Visual Studio CodeでC#コードをデバッグするための構成ファイル設定の基本](https://atmarkit.itmedia.co.jp/ait/articles/1709/01/news034_2.html)