采用 C# 的 Lambda 函数处理程序 - Amazon Lambda
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅中国的 Amazon Web Services 服务入门

采用 C# 的 Lambda 函数处理程序

Lambda 函数处理程序是函数代码中处理事件的方法。当调用函数时,Lambda 运行处理程序方法。当该处理程序退出或返回一个响应时,它便可用于处理另一个事件。

将 Lambda 函数处理程序定义为某个类中的实例或静态方法。如需访问 Lambda 上下文对象,您可以定义类型 ILambdaContext 的方法参数。您可以使用此方法来访问与当前调用相关的信息,例如函数的名称、内存限制、剩余的执行时间和日志记录。

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

在该语法中,需要注意以下方面:

  • inputType – 第一个处理程序参数为处理程序的输入。它可以是事件数据(由事件源发布)或您提供的自定义输入(如字符串或任意自定义数据对象)。

  • returnType – 如果计划同步调用 Lambda 函数(使用 RequestResponse 调用类型),则可以使用任何支持的数据类型返回函数输出。例如,如果使用 Lambda 函数作为移动应用程序后端,则同步调用该函数。您的输出数据类型将会序列化为 JSON。

    如果计划异步调用 Lambda 函数(使用 Event 调用类型),则 returnType 应为 void。例如,将 Lambda 搭配 Amazon Simple Storage Service (Amazon S3) 或 Amazon Simple Notification Service (Amazon SNS) 之类的事件源使用时,这些事件源会使用 Event 调用类型调用 Lambda 函数。

  • ILambdaContext context – 处理程序签名中的第二个参数是可选的。它提供对上下文对象的访问权限,该对象包含有关函数和请求的信息。

处理流

默认情况下,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

处理标准数据类型

以下所有其他类型均需要您指定序列化器:

  • Primitive .NET 类型(例如字符串或 int)

  • 集合和映射 - IList、IEnumerable、IList<T>、Array、IDictionary、IDictionary<TKey 和 TValue>

  • POCO 类型(无格式的旧 CLR 对象)

  • 预定义的Amazon事件类型

  • 对于异步调用,Lambda 将忽略返回类型。在这些情况下,可以将返回类型设置为 void。

  • 如果您使用的是 .NET 异步编程,则返回类型可以是 Task 和 Task<T>,并且可使用 asyncawait 关键字。有关更多信息,请参阅 将 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 Amazon .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 可以使用源生成器,以允许进行 JSON 解析而无需反映 API。这可以帮助提高冷启动性能。

使用源生成器

  1. 在您的项目中,定义从 System.Text.Json.Serialization.JsonSerializerContext 衍生的空部分类。

  2. 为源生成器必须为其生成序列化代码的每个 .NET 类型添加 JsonSerializable 属性。

例 利用原生成的 API 网关集成

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,其中:

  • 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 会将此程序集处理为可执行项,并在启动时执行它。您必须将 NuGet 程序包 Amazon.Lambda.RuntimeSupport 添加到项目,以便在启动时运行的可执行项启动 Lambda 运行时客户端。

序列化 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 库的一部分提供)创建您自己的序列化库。该接口定义了两种方法:

    • 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 文档网站上的不安全(C# 参考)

  • 处理程序签名不得使用 params 关键字传递可变数量的参数,也不得将用于支持可变数量参数的 ArgIterator 用作输入或返回参数。

  • 处理程序不能是泛型方法,例如,IList<T> Sort<T>(IList<T> input)。

  • 不支持使用签名 async void 的 Async 处理程序。

将 C# 函数中的 Async 与 Lambda 搭配使用

如果您知道自己的 Lambda 函数需要长时间的运行过程,例如上传大型文件到 Amazon S3 或者从 Amazon DynamoDB 中读取大量记录,则您可以利用 async/await 模式。当您使用此签名时,Lambda 会同步调用函数并等待该函数返回响应或执行超时。

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

如果使用此模式,请考虑以下事项:

  • Lambda 不支持 async void 方法。

  • 如果您创建了一个 async Lambda 函数,但未实施 await 运算符,.NET 将发出一个编译器,提醒您注意意外行为。例如,有些 async 操作会运行,而有些不会。或者,有些 async 操作不会在函数调用完成时结束。

    public async Task ProcessS3ImageResizeAsync(SimpleS3Event event) // Compiler warning { client.DoAsyncWork(input); }
  • 您的 Lambda 函数可包含多个可并行调用的 async 调用。您可使用 Task.WhenAllTask.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); }