Annotations Frameworkを使って、C#でのAWS Lambda 関数実装をもっと楽にしよう

# はじめに

Visual Studio で AWS Lambda の新しいプロジェクトを作成しようとしたら、`Annotations Framework` という見慣れないプロジェクトがありました。今回はこの子がどういったものであるかを確認していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/563bbcc8-1aa8-fe4b-e74d-7ac82ce28db2.png)

# Lambda Annotations とは？

AWS Labmda Dotnet の Lambda Annotations に関する [README](https://github.com/aws/aws-lambda-dotnet/blob/master/Docs/lambda-annotations-design.md) を確認すると、下記のような説明があります。

> ラムダ注釈デザインは、.NET ラムダ関数を記述するための新しいプログラミングモデルです。大まかに言うと、新しいプログラミング モデルでは慣用的な .NET コーディング パターンが可能になり、C# ソース ジェネレーター テクノロジを使用して、Lambda プログラミング モデルとより慣用的なプログラミング モデルの間のギャップを埋めます。

これまで、.NET で作った Lambda の関数を WebAPI として公開する場合、.NET 側の関数で API Gateway からコンテキストを受け取るように実装し、CloudFormation のテンプレートで APIGateway と Lambda 関数を定義してマッピングしてあげる必要がありました。

もしくは、WebAPI を公開したいという目的であれば、ASP.NET Core のアプリケーションそのものを Lambda 関数として動かして APIGateway と Proxy 統合する方法もあります。この場合は、素の ASP.NET Core のプログラミングモデルをそのまま使えるのでうれしいのですが、Lambda 関数のコールドスタート時に ASP.NET Core の初期化処理が入り時間がかかるので悩ましい部分がありました。

Lambda Annotations を使うと、.NET の関数に定義した属性の値をもとに CloudFormation のコードを自動生成してくれるようです。また、Lambda Annotations が対象にしているのは WebApi だけではなく、S3、SQS、DynamoDb を対象としたトリガーにも対応しているようです。

# Lambda Annotations プロジェクトを作成する。

Visual Studio で Annotations Framework を選択してプロジェクトを作成すると、5 個の Lambda 関数(Default, Add, Substract, Multiply, Divide)を定義した `Functions.cs` と、依存関係の注入の設定を行うための `Startup.cs` を含むプロジェクトが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/a3754cd5-480f-584c-3aa0-7396de14c412.png)

Visual Studio Code などを利用する場合、下記のコマンドで Lambda 用のテンプレートをインストールするとプロジェクトを作成できるようになります。

```sh
> dotnet new -i Amazon.Lambda.Templates::*
> dotnet new serverless.Annotations
```

プロジェクト作成直後のプロジェクトファイルは次のようになっています。
[Amazon.Lambda.Annotations](https://www.nuget.org/packages/Amazon.Lambda.Annotations) というパッケージが Annotations の本体のようです。`AWS .NET Lambda Mock Test Tool` 用の設定も入っているので、ローカルで Lambda 関数のデバックを利用できそうなのも良いですね。

```xml:AWSServerless1.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <AWSProjectType>Lambda</AWSProjectType>
    <!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

    <!-- Generate ready to run images during publishing to improvement cold starts. -->
    <PublishReadyToRun>true</PublishReadyToRun>	 
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="2.1.0" />
    <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.4.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.2.0" />
    <PackageReference Include="Amazon.Lambda.Annotations" Version="0.5.0-preview" />
  </ItemGroup>
</Project>
```

Amazon.Lambda.Annotations が依存関係として `net6.0` を持っているので、AWS AnnotatAWS Framework を利用する場合は `.NET 6.0` 以降が対象になるのは注意点です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/4741ace0-18ec-72e5-4201-ee096301d9de.png)

# Lambda 関数の中身を確認する

`Functions.cs` を簡単に見ていきましょう。
`Add` メソッドには、[Amazon.Lambda.Annotations.LambdaFunction](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Annotations/LambdaFunctionAttribute.cs) 属性と [Amazon.Lambda.Annotations.HttpApi](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Annotations/HttpApiAttribute.cs) 属性が付与されていることが確認できます。どうやらこれらの属性をもとにソースジェネレーターで CloudFormation テンプレートを更新するようですね。[Amazon.Lambda.Annotations 名前空間](https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.Annotations)を見ると、ほかにも利用できそうな属性を確認できます。

```cs:Functions.cs
using Amazon.Lambda.Core;
using Amazon.Lambda.Annotations;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace AWSServerless1
{
    /// <summary>
    /// A collection of sample Lambda functions that provide a REST api for doing simple math calculations. 
    /// </summary>
    public class Functions
    {
        public Functions()
        {
        }

        // ... 略 ...

        /// <summary>
        /// Perform x + y
        ///
        /// PackageType is currently required to be set to LambdaPackageType.Image till the upcoming .NET 6 managed
        /// runtime is available. Once the .NET 6 managed runtime is available PackageType will be optional and will
        /// default to Zip.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns>Sum of x and y.</returns>
        [LambdaFunction()]
        [HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
        public int Add(int x, int y, ILambdaContext context)
        {
            context.Logger.LogInformation($"{x} plus {y} is {x + y}");
            return x + y;
        }

        // ... 略 ...
    }
}
```        

ビルドすると、同じ階層の `serverless.template` が更新されます。
試しに Functions.cs に次のメソッドを追加してビルドして見ます。

```cs:Functions1
namespace AWSServerless1
{
    public class Functions
    {
        // .. 略 ...

        [LambdaFunction]
        [HttpApi(LambdaHttpMethod.Get, "/hello/{message}")]
        public string Hello(string message) => $"Hello {message}";

        // .. 略 ...
```        

`AWSServerless1::AWSServerless1.Functions_Hello_Generated::Hello` という関数と API Gateway の定義が追加されているのがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/b15aa15f-5a42-5990-cd90-978d28c7f2c4.png)

# Lambda Annotations のローカルでのデバック

[AWS Toolkit for Visual Studio](https://aws.amazon.com/jp/visualstudio/) か、[dotnet-lambda-test-tool](https://github.com/aws/aws-lambda-dotnet/blob/master/Tools/LambdaTestTool/README.md) がインストールされていればローカルで Lambda 関数のデバック実行が行えます。Visual Studio の場合、実行メニューで `Mock Lambda Test Tool` を指定してデバックを開始すればよいです。Visual Studio Code でデバックする場合は、[Configure for Visual Studio Code](https://github.com/aws/aws-lambda-dotnet/blob/master/Tools/LambdaTestTool/README.md#configure-for-visual-studio-code) の手順でデバックできそうです(未確認)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/3c46686d-ae87-3b44-04ef-8b96b4d6520e.png)

Functions に先ほど生成された `AWSServerless1.FunctionsHelloGenerated` を指定し、Function input に次のような JSON を指定して `Execute Function` ボタンをクリックすれば、Visual Studio でデバックすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/6d06f306-d90b-411c-a87e-401a35f5d3a3.png)

```json:Function input
{
    "version": "2.0",
    "routeKey": "GET /hello/{message}",
    "pathParameters": {
        "message": "world"
    },
    "isBase64Encoded": false
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/680f2ccb-9e6a-bf91-a262-06e6f6e7eb79.png)

# 依存関係の注入と設定情報の取得

Annotations Framework では、[Amazon.Lambda.Annotations.LambdaStartup](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Annotations/LambdaStartupAttribute.cs) 属性がついたクラスで依存関係の注入の設定を行うことができます。テンプレートで作成されたプロジェクトでは、`Startup.cs` に `LambdaStartup` 属性がついているのでこのクラスで設定を行っていきます。

ここで設定した依存関係は、各 Lambda 関数のコンストラクタか、メソッドで受け取ることができます。
例えば、IOptions<T> インターフェイスと、AWS S3 の呼び出しを行いたい場合は必要なパッケージを追加した後、

```
dotnet add package Microsoft.Extensions.Options
dotnet add package AWSSDK.Extensions.NETCore.Setup
dotnet add package AWSSDK.S3
```

ソリューションエクスプローラーから `appsettings.json` を追加して、次の設定ファイルを追加します。

```json:appsetting.json
{
  "Setting1": {
    "Prop1": "Hoge",
    "Prop2": "Hogege"
  },
  "AWS": {
    "Profile": "lab",
    "Region": "ap-northeast-1"
  }
}
```

Lambda 関数の初期化時に ConfigureServices メソッドで設定された依存関係が Lambda 関数のコンストラクタ、もしくはメソッドに依存関係として注入されます。

```cs:Startup.cs
    [Amazon.Lambda.Annotations.LambdaStartup]
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            var builder = new ConfigurationBuilder()
                    .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json")
                    .AddEnvironmentVariables();

            var configuration = builder.Build();
            services.AddSingleton<IConfiguration>(configuration);
            services.Configure<Setting1>(configuration.GetSection("Setting1"));

            services.AddDefaultAWSOptions(configuration.GetAWSOptions());
            services.AddAWSService<IAmazonS3>();
        }
    }

    public class Setting1
    {
        public string Prop1 { get; set; } = null!;
        public string Prop2 { get; set; } = null!;
    }
```

メソッドで依存関係を受ける場合、生存期間が Scope or Singleton の場合は `FromServices` 属性をつけないと例が発生するので注意してください。

```cs:Functions.cs
    public class Functions
    {
        private readonly IConfiguration _configuration;
        private readonly IOptions<Setting1> _options;
        public Functions(IConfiguration configuration, IOptions<Setting1> options)
        {
            _configuration = configuration;
            _options = options;
        }


        [LambdaFunction]
        [HttpApi(LambdaHttpMethod.Get, "/hello/{message}")]
        
        public string Hello(
            string message,
            [FromServices] IOptions<Setting1> options
        )
            => $"Hello {message} {_configuration.GetValue<string>("Setting1:Prop1")} {options.Value.Prop1} {_options.Value.Prop2}";
        
        // ... 略 ...
    }
```    

# おわりに

Annotation Framework いいですね！
まだ preview リリースなのでこれからも変更が入っていくと思いますが、早くリリースしてほしいです。