C# の Lambda 関数ハンドラー - AWS Lambda

C# の Lambda 関数ハンドラー

Lambda 関数のハンドラーは、イベントを処理する関数コード内のメソッドです。関数が呼び出されると、Lambda はハンドラーメソッドを実行します。ハンドラーによってレスポンスが終了するか、レスポンスが返ったら、別のイベントを処理できるようになります。

Lambda 関数のハンドラーをクラスのインスタンスまたは静的メソッドとして定義します。Lambda コンテキストオブジェクトにアクセスするために、タイプ ILambdaContext のメソッドパラメータを定義できます。これを使用して、関数の名前、メモリ制限、残りの実行時間、ログ記録など、現在の呼び出しに関する情報にアクセスできます。

returnType handler-name(inputType input, ILambdaContext context) { ... }

構文では、以下の点に注意してください。

  • inputType – 最初のハンドラーパラメータはハンドラーへの入力です。これは、(イベントソースが公開する) イベントデータ、または文字列や任意のカスタムデータオブジェクトなど、指定したカスタム入力にすることができます。

  • returnType Lambda 関数を同期的に呼び出す (RequestResponse 呼び出しタイプを使用) 場合は、サポートされているいずれかのデータ型を使用して関数の出力を返すことができます。例えば、Lambda 関数をモバイルアプリケーションのバックエンドとして使用する場合は、これを同期的に呼び出しています。出力データ型は JSON にシリアル化されます。

    Lambda 関数を非同期的に呼び出す場合 (Event 呼び出しタイプを使用)、returnTypevoid である必要があります。例えば、Amazon Simple Storage Service (Amazon S3) や Amazon Simple Notification Service (Amazon SNS) などのイベントソースで Lambda を使用する場合、これらのイベントソースは Event 呼び出しタイプを使用して Lambda 関数を呼び出します。

  • ILambdaContext context - ハンドラー署名の 2 番目の引数はオプションです。これは、関数とリクエストに関する情報を持つコンテキストオブジェクトへのアクセスを提供します。

ストリームの処理

デフォルトでは、Lambda は入力パラメータとして System.IO.Stream タイプのみをサポートします。

たとえば、次の C# コードの例を考えてみます。

using System.IO; namespace Example { public class Hello { public Stream MyHandler(Stream stream) { //function logic } } }

C# コードの例では、最初のハンドラーパラメータはハンドラー (MyHandler) への入力です。これは、イベントデータ (Amazon S3 などのイベントソースによって公開されたもの)、または Stream (この例のように) や任意のカスタムデータオブジェクトなど、指定したカスタム入力にすることができます。出力は Stream 型となります。

標準データ型の処理

次の他のタイプについては、すべてシリアライザーを指定する必要があります。

  • .NET プリミティブ型 (文字列、整数など)

  • コレクションとマップ – IList、IEnumerable、IList<T>、Array、IDictionary、IDictionary<TKey, TValue>

  • POCO (Plain old CLR objects) 型

  • 定義済みの AWS イベントタイプ

  • 非同期呼び出しの場合、Lambda は戻り型を無視します。このような場合、戻り型を void に設定することもできます。

  • .NET 非同期プログラミングを使用している場合、戻り型は Task および Task<T> 型で async および await キーワードを使用できます。詳細については、「C# 関数の async と Lambda の併用」を参照してください。

関数の入力パラメータと出力パラメータがタイプ System.IO.Stream でない限り、それらをシリアル化する必要があります。Lambda は、アプリケーションのアセンブリまたはメソッドレベルで適用できるデフォルトのシリアライザーを提供します。または、Amazon.Lambda.Core ライブラリが提供する ILambdaSerializer インターフェイスを実装することで独自のシリアライザーを定義できます。詳細については、「.zip ファイルアーカイブを使用して C# Lambda 関数をデプロイする」を参照してください。

メソッドにデフォルトのシリアライザー属性を追加するには、最初に Amazon.Lambda.Serialization.SystemTextJson ファイルの .csproj に依存関係を追加します。

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> <AWSProjectType>Lambda</AWSProjectType> <!-- 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 improve cold start time. --> <PublishReadyToRun>true</PublishReadyToRun> </PropertyGroup> <ItemGroup> <PackageReference Include="Amazon.Lambda.Core" Version="2.1.0 " /> <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.2.0" /> </ItemGroup> </Project>

以下の例では、あるメソッドでデフォルトの System.Text.Json シリアライザーを指定し、別のメソッドで選択したものを指定することで、柔軟な利用が可能であることを示しています。

public class ProductService { [LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] public Product DescribeProduct(DescribeProductRequest request) { return catalogService.DescribeProduct(request.Id); } [LambdaSerializer(typeof(MyJsonSerializer))] public Customer DescribeCustomer(DescribeCustomerRequest request) { return customerService.DescribeCustomer(request.Id); } }

JSON シリアル化のソース生成

C# 9 は、コンパイル中のコード生成を許可するソースジェネレーターを提供します。.NET 6 以降、ネイティブ JSON ライブラリ System.Text.Json はソースジェネレータを使用できるため、リフレクション API を必要とせずに JSON 解析が可能になります。これにより、コールドスタートのパフォーマンスを向上させることができます。

ソースジェネレータを使用するには

  1. プロジェクトで、System.Text.Json.Serialization.JsonSerializerContext から派生する空の部分クラスを定義します。

  2. ソースジェネレータがシリアル化コードを生成する必要がある各 .NET タイプの JsonSerializable 属性を追加します。

例 ソース生成を活用した API Gateway 統合

using System.Collections.Generic; using System.Net; using System.Text.Json.Serialization; using Amazon.Lambda.Core; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Serialization.SystemTextJson; [assembly: LambdaSerializer(typeof(SourceGeneratorLambdaJsonSerializer<SourceGeneratorExa mple.HttpApiJsonSerializerContext>))] namespace SourceGeneratorExample; [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] public partial class HttpApiJsonSerializerContext : JsonSerializerContext { } public class Functions { public APIGatewayProxyResponse Get(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) { context.Logger.LogInformation("Get Request"); var response = new APIGatewayHttpApiV2ProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = "Hello AWS Serverless", Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } } }; return response; } }

関数を呼び出すと、Lambda はソースが生成した JSON シリアル化コードを使用して、Lambda イベントとレスポンスのシリアル化を処理します。

ハンドラー署名

Lambda 関数を作成する際は、呼び出すコードをどこで探すかを Lambda に指示するハンドラー文字列を指定する必要があります。C# では、次の形式になります。

ASSEMBLY::TYPE::METHOD where:

  • ASSEMBLY は、アプリケーションの .NET アセンブリファイルの名前です。.NET Core CLI を使用してアプリケーションをビルドする場合、.csproj ファイルで AssemblyName プロパティを使用してアセンブリ名を設定しないと、ASSEMBLY 名は .csproj ファイルの名前になります。詳細については、「.NET Core CLI」を参照してください。ここでは、.csproj ファイルは HelloWorldApp.csproj であると仮定します。

  • TYPE は、NamespaceClassName からなるハンドラー型の正式名称となります。この場合、Example.Hello

  • METHOD は、関数ハンドラー名で、この場合 MyHandler です。

最終的に、署名の形式は、Assembly::Namespace.ClassName::MethodName となります。

次の例を考えます。

using System.IO; namespace Example { public class Hello { public Stream MyHandler(Stream stream) { //function logic } } }

ハンドラー文字列は次のようになります。 HelloWorldApp::Example.Hello::MyHandler

重要

ハンドラ文字列で指定されたメソッドが過負荷になっている場合は、Lambda が呼び出す必要があるメソッドの正確な署名を指定する必要があります。解決で複数の (過負荷の) 署名から選択することが求められる場合、Lambda は、その他の有効な署名を拒否します。

最上位ステートメントの使用

.NET 6 以降では、最上位のステートメントを使用して関数を記述できます。最上位レベルのステートメントは、.NET プロジェクトに必要な定型コードの一部を削除し、記述するコードの行数を減らします。例えば、トップレベルのステートメントを使用して前の例を書き直すことができます。

using Amazon.Lambda.RuntimeSupport; var handler = (Stream stream) => { //function logic }; await LambdaBootstrapBuilder.Create(handler).Build().RunAsync();

最上位のステートメントを使用する場合は、ハンドラーの署名を提供するときにのみ ASSEMBLY の名前を含めます。前の例から引き続き、ハンドラー文字列は HelloWorldApp になります。

ハンドラーをアセンブリ名に設定すると、Lambda はアセンブリを実行可能として扱い、起動時に実行します。起動時に実行される実行可能ファイルが Lambda ランタイムクライアントを起動するように、NuGet パッケージ Amazon.Lambda.RuntimeSupport をプロジェクトに追加する必要があります。

Lambda 関数のシリアル化

Stream オブジェクト以外の入出力タイプを使用するすべての Lambda 関数には、アプリケーションにシリアル化ライブラリを追加する必要があります。これは以下の方法で対応できます。

  • Amazon.Lambda.Serialization.SystemTextJson NuGet パッケージを使用します。このライブラリは、ネイティブの .NET Core JSON シリアライザーを使用してシリアル化を処理します。このパッケージは、Amazon.Lambda.Serialization.Json のパフォーマンスを向上させますが、Microsoft ドキュメントに記載されている制限に注意してください。このライブラリは、.NET Core 3.1 以降のランタイムで使用できます。

  • Amazon.Lambda.Serialization.Json NuGet パッケージを使用します。このライブラリでは、シリアル化を処理するために JSON.NET が使用されます。

  • ILambdaSerializer インターフェイスの実装によって独自のシリアル化ライブラリを作成します。これは、Amazon.Lambda.Core ライブラリの一部として入手できます。インターフェイスは 2 つのメソッドを定義します。

    • T Deserialize<T>(Stream requestStream);

      このメソッドを実装して、Invoke API から Lambda 関数ハンドラーに渡されるオブジェクトにリクエストのペイロードを逆シリアル化することができます。

    • T Serialize<T>(T response, Stream responseStream);.

      このメソッドを実装して、Lambda 関数のハンドラーから返される結果を Invoke API オペレーションが返すレスポンスペイロードにシリアル化することができます。

シリアライザーを使用するには、MyProject.csproj ファイルに依存関係を追加する必要があります。

... <ItemGroup> <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.1.0" /> <!-- or --> <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="2.0.0" /> </ItemGroup>

次に、シリアライザーを定義する必要があります。次の例では、AssemblyInfo.cs ファイルで Amazon.Lambda.Serialization.SystemTextJson シリアライザーを定義します。

[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]

次の例では、AssemblyInfo.cs ファイルで Amazon.Lambda.Serialization.Json シリアライザーを定義します。

[assembly: LambdaSerializer(typeof(JsonSerializer))]

メソッドレベルでカスタムシリアル化属性を定義することができます。それにより、アセンブリレベルで指定されたデフォルトのシリアライザーが上書きされます。

public class ProductService{ [LambdaSerializer(typeof(JsonSerializer))] public Product DescribeProduct(DescribeProductRequest request) { return catalogService.DescribeProduct(request.Id); } [LambdaSerializer(typeof(MyJsonSerializer))] public Customer DescribeCustomer(DescribeCustomerRequest request) { return customerService.DescribeCustomer(request.Id); } }

Lambda 関数ハンドラーの制限

ハンドラー署名にはいくつかの制限があることに注意してください。

  • ハンドラーメソッドとその依存関係内で unsafe コンテキストを使用できますが、unsafe ではなく、ハンドラー署名でポインター型を使用する場合があります。詳細については、Microsoft Docs ウェブサイトの「unsafe (C# Reference)」を参照してください。

  • params キーワードを使用して可変数のパラメータを渡さなかったり、可変数のパラメータをサポートするために使用する入力パラメータまたは戻りパラメータとして ArgIterator を使用できない場合があります。

  • ハンドラーは、IList<T> Sort<T>(IList<T> input) などの汎用メソッドではない場合があります。

  • async void 署名を持つ Async ハンドラーはサポートされていません。

C# 関数の async と Lambda の併用

大容量ファイルを Amazon S3 にアップロードする、または Amazon DynamoDB から大量のストリームのレコードを読み取るなど、Lambda 関数で長時間実行するプロセスが必要になることがわかっている場合は、async/await パターンを利用できます。この署名を使用すると、Lambda は関数を同期的に呼び出し、関数からレスポンスが返るまで、または実行がタイムアウトするまで待機します。

public async Task<Response> ProcessS3ImageResizeAsync(SimpleS3Event input) { var response = await client.DoAsyncWork(input); return response; }

このパターンを使用する場合は、次の事項を考慮します。

  • Lambda は async void メソッドをサポートしていません。

  • await 演算子を実装しないで非同期 Lambda 関数を作成すると、.NET がコンパイラの警告を出し、予期しない動作が発生します。例えば、一部の非同期アクションが実行されないことがあります。また、関数の呼び出しが完了する前に、一部の非同期アクションが完了しないことがあります。

    public async Task ProcessS3ImageResizeAsync(SimpleS3Event event) // Compiler warning { client.DoAsyncWork(input); }
  • Lambda 関数は、並行で呼び出すことができる、複数の非同期呼び出しを含めることができます。複数のタスクを使用するため、Task.WhenAll および Task.WhenAny メソッドを使用できます。Task.WhenAll メソッドを使用するには、配列としてオペレーションのリストをメソッドに渡します。以下の例では、配列へ一部の操作を含めるのを怠ると、その操作が完了する前に呼び出しが返されることに注意してください。

    public async Task DoesNotWaitForAllTasks1() { // In Lambda, Console.WriteLine goes to CloudWatch Logs. var task1 = Task.Run(() => Console.WriteLine("Test1")); var task2 = Task.Run(() => Console.WriteLine("Test2")); var task3 = Task.Run(() => Console.WriteLine("Test3")); // Lambda may return before printing "Test2" since we never wait on task2. await Task.WhenAll(task1, task3); }

    Task.WhenAny メソッドを使用するには、配列としてオペレーションのリストを再度メソッドに渡します。最初の操作が完了すると、他がまだ実行中でも呼び出しがすぐに返されます。

    public async Task DoesNotWaitForAllTasks2() { // In Lambda, Console.WriteLine goes to CloudWatch Logs. var task1 = Task.Run(() => Console.WriteLine("Test1")); var task2 = Task.Run(() => Console.WriteLine("Test2")); var task3 = Task.Run(() => Console.WriteLine("Test3")); // Lambda may return before printing all tests since we're waiting for only one to finish. await Task.WhenAny(task1, task2, task3); }