Definir el controlador de funciones de Lambda en C# - AWS Lambda

Definir el controlador de funciones de Lambda en C#

El controlador de la función de Lambda es el método del código de la función que procesa eventos. Cuando se invoca una función, Lambda ejecuta el método del controlador. La función se ejecuta hasta que el controlador devuelve una respuesta, se cierra o se agota el tiempo de espera.

Cuando se invoca la función y Lambda ejecuta el método controlador de la función, pasa dos argumentos a la función. El primer argumento es el objeto de event. Cuando otro Servicio de AWS invoca la función, el objeto event contiene datos sobre el evento que provocó la invocación de la función. Por ejemplo, un objeto event de API Gateway contiene información sobre la ruta, el método HTTP y los encabezados HTTP. La estructura exacta del evento varía en función del Servicio de AWS que esté invocando la función. Consulte Invocar Lambda con eventos de otros servicios de AWS para obtener más información sobre los formatos de eventos para los servicios individuales.

Lambda también pasa un objeto context a la función. El objeto proporciona información sobre la invocación, la función y el entorno de ejecución. Para obtener más información, consulte Uso del objeto de contexto de Lambda para recuperar información de funciones de C#.

El formato nativo de todos los eventos de Lambda son secuencias de bytes que representan el evento con formato JSON. A menos que los parámetros de entrada y salida de la función sean del tipo System.IO.Stream, debe serializarlos. Especifique el serializador que quiere usar configurando el atributo de conjunto LambdaSerializer. Para obtener más información, consulte Serialización de las funciones de Lambda.

Modelos de ejecución de .NET para Lambda

Existen dos modelos de ejecución diferentes para ejecutar funciones de Lambda en .NET: el enfoque de biblioteca de clases y el enfoque de conjunto ejecutable.

En el enfoque de biblioteca de clases, se proporciona a Lambda una cadena que indica el AssemblyName, ClassName y Method de la función que se va a invocar. Para obtener más información sobre el formato de esta cadena, consulte Controladores de bibliotecas de clases. Durante la fase de inicialización de la función, se inicializa la clase de la función y se ejecuta cualquier código del constructor.

En el enfoque de conjunto ejecutable, se utiliza la característica de declaraciones de nivel superior de C# 9. Este enfoque genera un conjunto ejecutable que Lambda ejecuta cada vez que recibe un comando invoke para su función. Solo debe proporcionar a Lambda el nombre del conjunto ejecutable que se va a ejecutar.

En las siguientes secciones se ofrece un ejemplo de código de función para estos dos enfoques.

Controladores de bibliotecas de clases

El siguiente código de función de Lambda muestra un ejemplo de un método de controlador (FunctionHandler) para una función de Lambda que utiliza el enfoque de biblioteca de clases. En esta función de ejemplo, Lambda recibe un evento de API Gateway que invoca la función. La función lee un registro de una base de datos y lo devuelve como parte de la respuesta de API Gateway.

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace GetProductHandler; public class Function { private readonly IDatabaseRepository _repo; public Function() { this._repo = new DatabaseRepository(); } public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request) { var id = request.PathParameters["id"]; var databaseRecord = await this._repo.GetById(id); return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = JsonSerializer.Serialize(databaseRecord) }; } }

Al crear una función de Lambda, debe proporcionar a Lambda información sobre el controlador de la función en forma de cadena de controlador. Esto le indica a Lambda qué método del código debe ejecutar cuando se invoque la función. En C#, el formato de la cadena del controlador cuando se utiliza el enfoque de biblioteca de clases es el siguiente:

ASSEMBLY::TYPE::METHOD, donde:

  • ASSEMBLY es el nombre del archivo de conjunto de .NET para la aplicación. Si utiliza la CLI de Amazon.Lambda.Tools para compilar la aplicación y no establece el nombre del ensamblado con la propiedad AssemblyName del archivo .csproj, ASSEMBLY será simplemente el nombre de la carpeta que contiene el archivo .csproj.

  • TYPE es el nombre completo del tipo de controlador, que consta de Namespace y ClassName.

  • METHOD es el nombre del método de controlador en el código de la función.

En el código de ejemplo que se muestra, si se nombra el conjunto como GetProductHandler, entonces la cadena del controlador sería GetProductHandler::GetProductHandler.Function::FunctionHandler.

Controladores de conjuntos ejecutables

En el siguiente ejemplo, la función de Lambda se define como un conjunto ejecutable. El método controlador de este código recibe el nombre Handler. Cuando se utilizan ensamblados ejecutables, se debe iniciar el tiempo de ejecución de Lambda. Para ello, se utiliza el método LambdaBootstrapBuilder.Create. Este método toma como entradas el método que su función usa como controlador y el serializador Lambda que debe usar.

Para obtener más información sobre el uso de instrucciones de nivel superior, consulte Introducción del tiempo de ejecución .NET 6 de AWS Lambda en el Blog de computación de AWS.

namespace GetProductHandler; IDatabaseRepository repo = new DatabaseRepository(); await LambdaBootstrapBuilder.Create<APIGatewayProxyRequest>(Handler, new DefaultLambdaJsonSerializer()) .Build() .RunAsync(); async Task<APIGatewayProxyResponse> Handler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) { var id = apigProxyEvent.PathParameters["id"]; var databaseRecord = await this.repo.GetById(id); return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = JsonSerializer.Serialize(databaseRecord) }; };

Cuando se utilizan conjuntos ejecutables, la cadena del controlador que indica a Lambda cómo ejecutar el código es el nombre del conjunto. En este ejemplo, eso sería GetProductHandler.

Serialización de las funciones de Lambda

Si las funciones de Lambda que utilizan tipos de entrada o salida distintos de un objeto Stream, debe agregar una biblioteca de serialización a la aplicación. Puede implementar la serialización mediante la serialización estándar basada en la reflexión proporcionada por System.Text.Json y Newtonsoft.Json, o mediante la serialización generada en la fuente.

Uso de la serialización generada en la fuente

La serialización generada en la fuente es una característica de las versiones 6 y posteriores de .NET que permite generar el código de serialización en tiempo de compilación. Elimina la necesidad de reflexión y puede mejorar el rendimiento de la función. Para utilizar la serialización generada en la fuente en la función, haga lo siguiente:

  • Cree una nueva clase parcial que herede de JsonSerializerContext al añadir atributos JsonSerializable para todos los tipos que requieran serialización o deserialización.

  • Configurar la LambdaSerializer para usar un SourceGeneratorLambdaJsonSerializer<T>.

  • Actualice cualquier serialización o deserialización manual del código de su aplicación para usar la clase recién creada.

En el siguiente código se muestra un ejemplo de función que utiliza la serialización generada en la fuente.

[assembly: LambdaSerializer(typeof(SourceGeneratorLambdaJsonSerializer<CustomSerializer>))] public class Function { private readonly IDatabaseRepository _repo; public Function() { this._repo = new DatabaseRepository(); } public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request) { var id = request.PathParameters["id"]; var databaseRecord = await this._repo.GetById(id); return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = JsonSerializer.Serialize(databaseRecord, CustomSerializer.Default.Product) }; } } [JsonSerializable(typeof(APIGatewayProxyRequest))] [JsonSerializable(typeof(APIGatewayProxyResponse))] [JsonSerializable(typeof(Product))] public partial class CustomSerializer : JsonSerializerContext { }
nota

Si desea utilizar la compilación anticipada (AOT) nativa con Lambda, debe usar la serialización generada en la fuente.

Uso de la serialización basada en la reflexión

AWS proporciona bibliotecas prediseñadas que le permiten añadir rápidamente la serialización a su aplicación. Esto se configura mediante los paquetes NuGet Amazon.Lambda.Serialization.SystemTextJson o Amazon.Lambda.Serialization.Json. Entre bastidores, Amazon.Lambda.Serialization.SystemTextJson utiliza System.Text.Json para realizar tareas de serialización y Amazon.Lambda.Serialization.Json utiliza el paquete Newtonsoft.Json.

También puede crear su propia biblioteca de serialización mediante la implementación de la interfaz ILambdaSerializer, que está disponible como parte de la biblioteca Amazon.Lambda.Core. Esta interfaz define dos métodos:

  • T Deserialize<T>(Stream requestStream);

    Puede implementar este método para deserializar la carga de solicitud desde la API Invoke en el objeto que se pasa al controlador de la función de Lambda.

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

    Puede implementar este método para serializar el resultado que devuelve el controlador de la función de Lambda en la carga de respuesta que devuelve la API de operación Invoke.

Simplifique el código de funciones con el marco de anotaciones Lambda

Las anotaciones Lambda son un marco de trabajo para .NET 6 y .NET 8 que simplifica la escritura de funciones de Lambda mediante C#. Con el marco de anotaciones, puede reemplazar gran parte del código de una función de Lambda escrita con el modelo de programación normal. El código escrito con el marco utiliza expresiones más sencillas que le permiten centrarse en la lógica empresarial.

El siguiente código de ejemplo muestra cómo el uso del marco de anotaciones puede simplificar la escritura de funciones de Lambda. El primer ejemplo muestra el código escrito con el modelo de programa Lambda normal y el segundo muestra el equivalente con el marco de anotaciones.

public APIGatewayHttpApiV2ProxyResponse LambdaMathAdd(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) { if (!request.PathParameters.TryGetValue("x", out var xs)) { return new APIGatewayHttpApiV2ProxyResponse { StatusCode = (int)HttpStatusCode.BadRequest }; } if (!request.PathParameters.TryGetValue("y", out var ys)) { return new APIGatewayHttpApiV2ProxyResponse { StatusCode = (int)HttpStatusCode.BadRequest }; } var x = int.Parse(xs); var y = int.Parse(ys); return new APIGatewayHttpApiV2ProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = (x + y).ToString(), Headers = new Dictionary≪string, string> { { "Content-Type", "text/plain" } } }; }
[LambdaFunction] [HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")] public int Add(int x, int y) { return x + y; }

Para ver otro ejemplo de cómo el uso de anotaciones Lambda puede simplificar su código, consulte este ejemplo de aplicación multiservicio en el repositorio de GitHub awsdocs/aws-doc-sdk-examples. La carpeta PamApiAnnotations usa anotaciones Lambda en el archivo principal function.cs. A modo de comparación, la carpeta PamApi tiene archivos equivalentes escritos con el modelo de programación normal de Lambda.

El marco de anotaciones utiliza generadores de código fuente para generar código que se traduce del modelo de programación Lambda al código que se ve en el segundo ejemplo.

Para obtener más información acerca de cómo utilizar anotaciones de Lambda para .NET, consulte los recursos siguientes:

Inyección de dependencias con el marco de anotaciones Lambda

También puede utilizar el marco de anotaciones de Lambda para añadir una inyección de dependencias a las funciones de Lambda mediante una sintaxis con la que esté familiarizado. Al añadir un atributo [LambdaStartup] a un archivo Startup.cs, la estructura de anotaciones Lambda generará el código necesario en tiempo de compilación.

[LambdaStartup] public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IDatabaseRepository, DatabaseRepository>(); } }

La función de Lambda puede inyectar servicios mediante la inyección de un constructor o mediante la inyección en métodos individuales mediante el atributo [FromServices].

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace GetProductHandler; public class Function { private readonly IDatabaseRepository _repo; public Function(IDatabaseRepository repo) { this._repo = repo; } [LambdaFunction] [HttpApi(LambdaHttpMethod.Get, "/product/{id}")] public async Task<Product> FunctionHandler([FromServices] IDatabaseRepository repository, string id) { return await this._repo.GetById(id); } }

Restricciones del controlador de funciones de Lambda

Tenga en cuenta que existen algunas restricciones que afectan a la firma del controlador.

  • No puede ser unsafe ni utilizar tipos de puntero en la signatura del controlador, aunque puede utilizar el contexto unsafe dentro del método del controlador y sus dependencias. Para obtener más información, consulte inseguro (Referencia de C#) en el sitio web de Microsoft Docs.

  • No puede pasar un número variable de parámetros utilizando la palabra clave params, ni utilizar ArgIterator como parámetro de entrada o de retorno que se utiliza para admitir un número variable de parámetros.

  • El controlador no puede ser un método genérico, por ejemplo, IList<T> Sort<T(IList<T> input).

  • No se admiten los controladores asíncronos con la signatura async void.

Prácticas recomendadas de codificación para las funciones de Lambda en C#

Siga las directrices de la siguiente lista para utilizar las prácticas recomendadas de codificación al crear sus funciones de Lambda:

  • Separe el controlador de Lambda de la lógica del núcleo. Esto le permite probar las distintas unidades de la función con mayor facilidad.

  • Controle las dependencias del paquete de implementación de la función. El entorno de ejecución AWS Lambda contiene varias bibliotecas. Para disponer del conjunto más reciente de características y actualizaciones de seguridad, Lambda actualizará periódicamente estas bibliotecas. Estas actualizaciones pueden introducir cambios sutiles en el comportamiento de la función de Lambda. Para disponer de un control total de las dependencias que utiliza la función, empaquete todas las dependencias con el paquete de implementación.

  • Minimice la complejidad de las dependencias. Son preferibles los marcos de trabajo más sencillos, ya que se cargan rápidamente al arrancar el entorno de ejecución.

  • Minimice el tamaño del paquete de implementación de acuerdo con las necesidades de su tiempo de ejecución. Esto reducirá la cantidad de tiempo que tarda el paquete de implementación en descargarse y desempaquetarse antes de la invocación. En las funciones creadas en .NET, evite cargar toda la biblioteca del SDK de AWS como parte del paquete de implementación. En lugar de ello, cree dependencias selectivas de los módulos que seleccionen los componentes del SDK que necesita (por ejemplo, DynamoDB, módulos del SDK de Amazon S3 y bibliotecas básicas de Lambda).

  • Reutilice el entorno de ejecución para mejorar el rendimiento de la función. Inicialice los clientes de SDK y las conexiones de base de datos fuera del controlador de funciones y almacene localmente en caché los recursos estáticos en el directorio /tmp. Las invocaciones posteriores procesadas por la misma instancia de su función pueden reutilizar estos recursos. Esto ahorra costes al reducir el tiempo de ejecución de la función.

    Para evitar posibles filtraciones de datos entre las invocaciones, no utilice el entorno de ejecución para almacenar datos de usuario, eventos u otra información con implicaciones de seguridad. Si su función se basa en un estado mutable que no se puede almacenar en la memoria dentro del controlador, considere crear una función independiente o versiones independientes de una función para cada usuario.

  • Utilice una directiva keep-alive para mantener conexiones persistentes. Lambda purga las conexiones inactivas a lo largo del tiempo. Si intenta reutilizar una conexión inactiva al invocar una función, se producirá un error de conexión. Para mantener la conexión persistente, use la directiva keep-alive asociada al tiempo de ejecución. Para ver un ejemplo, consulte Reutilización de conexiones con Keep-Alive en Node.js.

  • Utilice variables de entorno para pasar parámetros operativos a su función. Por ejemplo, si está escribiendo en un bucket de Amazon S3, en lugar de codificar de forma rígida el nombre del bucket, configúrelo como una variable de entorno.

  • Evite utilizar invocaciones recursivas en la función de Lambda, en las que la función se invoca a sí misma o inicia un proceso que puede volver a invocarla. Esto podría producir un volumen no intencionado de invocaciones de la función y costos elevados. Si observa un volumen imprevisto de invocaciones, establezca la simultaneidad reservada de funciones en 0 inmediatamente para limitar todas las invocaciones de la función mientras actualiza el código.

  • No utilice API no documentadas y no públicas en el código de la función de Lambda. Para tiempos de ejecución administrados de AWS Lambda, Lambda aplica periódicamente actualizaciones funcionales y de seguridad a las API internas de Lambda. Estas actualizaciones de las API internas pueden ser incompatibles con versiones anteriores, lo que conlleva consecuencias no deseadas, como errores de invocación si su función depende de estas API no públicas. Consulte la referencia de la API para obtener una lista de las API disponibles públicamente.

  • Escriba el código idempotente. Escribir el código idempotente para las funciones garantiza que los eventos duplicados se gestionen de la misma manera. El código debe validar y gestionar correctamente los eventos duplicados. Para obtener más información, consulte ¿Cómo puedo hacer que mi función de Lambda sea idempotente?.