Define Lambda function handler in C# - Amazon Lambda
Services or capabilities described in Amazon Web Services documentation might vary by Region. To see the differences applicable to the China Regions, see Getting Started with Amazon Web Services in China (PDF).

Define Lambda function handler in C#

The Lambda function handler is the method in your function code that processes events. When your function is invoked, Lambda runs the handler method. Your function runs until the handler returns a response, exits, or times out.

When your function is invoked and Lambda runs your function's handler method, it passes two arguments to your function. The first argument is the event object. When another Amazon Web Service invokes your function, the event object contains data about the event that caused your function to be invoked. For example, an event object from API Gateway contains information about the path, HTTP method, and HTTP headers. The exact event structure varies according to the Amazon Web Service invoking your function. See Invoking Lambda with events from other Amazon services for more information about event formats for individual services.

Lambda also passes a context object to your function. This object contains information about the invocation, function, and execution environment. For more information, see Using the Lambda context object to retrieve C# function information.

The native format for all Lambda events is streams of bytes representing the JSON formatted event. Unless your function input and output parameters are of type System.IO.Stream, you must serialize them. Specify the serializer you want to use by setting the LambdaSerializer assembly attribute. For more information, see Serialization in Lambda functions.

.NET execution models for Lambda

There are two different execution models for running Lambda functions in .NET: the class library approach and the executable assembly approach.

In the class library approach, you provide Lambda with a string indicating the AssemblyName, ClassName, and Method of the function to be invoked. For more information about the format of this string, see Class library handlers. During the function's initialization phase, your function's class is initialized, and any code in the constructor is run.

In the executable assembly approach, you use C# 9's top-level statements feature. This approach generates an executable assembly which Lambda runs whenever it receives an invoke command for your function. You provide Lambda only with the name of the executable assembly to run.

The following sections give example function code for these two approaches.

Class library handlers

The following Lambda function code shows an example of a handler method (FunctionHandler) for a Lambda function which uses the class library approach. In this example funtion, Lambda receives an event from API Gateway that invokes the function. The function reads a record from a database and returns the record as part of the API Gateway response.

[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) }; } }

When you create a Lambda function, you need to provide Lambda with information about your function's handler in the form of a handler string. This tells Lambda which method in your code to run when your function is invoked. In C#, the format of the handler string when using the class library approach is as follows:

ASSEMBLY::TYPE::METHOD, where:

  • ASSEMBLY is the name of the .NET assembly file for your application. If you use the Amazon.Lambda.Tools CLI to build your application and you don't set the assembly name using the AssemblyName property in the .csproj file, then ASSEMBLY is simply the name of your .csproj file.

  • TYPE is the full name of the handler type, which consists of the Namespace and the ClassName.

  • METHOD is the name of the function handler method in your code.

For the example code shown, if the assembly is named GetProductHandler, then the handler string would be GetProductHandler::GetProductHandler.Function::FunctionHandler.

Executable assembly handlers

In the following example, the Lambda function is defined as an executable assembly. The handler method in this code is named Handler. When using executable assemblies, the Lambda runtime must be bootstrapped. To do this, you use the LambdaBootstrapBuilder.Create method. This method takes as inputs the method your function uses as the handler and the Lambda serializer to use.

For more information about using top-level statements, see Introducing the .NET 6 runtime for Amazon Lambda on the Amazon compute blog.

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 = input.PathParameters["id"]; var databaseRecord = await this.repo.GetById(id); return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = JsonSerializer.Serialize(databaseRecord) }; };

When using executable assemblies, the handler string that tells Lambda how to run your code is the name of the assembly. In this example, that would be GetProductHandler.

Serialization in Lambda functions

If your Lambda function uses input or output types other than a Stream object, you must add a serialization library to your application. You can implement serialization either using the standard reflection based serialization provided by System.Text.Json and Newtonsoft.Json, or by using source generated serialization.

Using source generated serialization

Source generated serialization is a feature of .NET versions 6 and later that allows serialization code to be generated at compile time. It removes the need for reflection and can improve the performance of your function. To use source generated serialization in your function, do the following:

  • Create a new partial class that inherits from JsonSerializerContext, adding JsonSerializable attributes for all types that require serialization or deserialization.

  • Configure the LambdaSerializer to use a SourceGeneratorLambdaJsonSerializer<T>.

  • Update any manual serialization or deserialization in your application code to use the newly created class.

An example function using source generated serialization is shown in the following code.

[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 { }
Note

If you want to use native ahead of time compilation (AOT) with Lambda, you must use source generated serialization.

Using reflection-based serialization

Amazon provides pre-built libraries to let you quickly add serialization to your application. You configure this using either the Amazon.Lambda.Serialization.SystemTextJson or Amazon.Lambda.Serialization.Json NuGet packages. Behind the scenes, Amazon.Lambda.Serialization.SystemTextJson uses System.Text.Json to perform serialization tasks, and Amazon.Lambda.Serialization.Json uses the Newtonsoft.Json package.

You can also create your own serialization library by implementing the ILambdaSerializer interface, which is available as part of the Amazon.Lambda.Core library. This interface defines two methods:

  • T Deserialize<T>(Stream requestStream);

    You implement this method to deserialize the request payload from the Invoke API into the object that is passed to your Lambda function handler.

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

    You implement this method to serialize the result returned from your Lambda function handler into the response payload that the Invoke API operation returns.

Simplify function code with the Lambda Annotations framework

Lambda Annotations is a framework for .NET 6 and .NET 8 which simplifies writing Lambda functions using C#. With the Annotations framework, you can replace much of the code in a Lambda function written using the regular programming model. Code written using the framework uses simpler expressions that allow you to focus on your business logic.

The following example code shows how using the annotations framework can simplify writing Lambda functions. The first example shows code written using the regular Lambda program model, and the second shows the equivalent using the Annotations framework.

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; }

For another example of how using Lambda Annotations can simplify your code, see this cross-service example application in the awsdocs/aws-doc-sdk-examples GitHub repository. The folder PamApiAnnotations uses Lambda Annotations in the main function.cs file. For comparison, the PamApi folder has equivalent files written using the regular Lambda programming model.

The Annotations framework uses source generators to generate code that translates from the Lambda programming model to the code seen in the second example.

For more information about how to use Lambda Annotations for .NET, see the following resources:

Dependency injection with Lambda Annotations framework

You can also use the Lambda Annotations framework to add dependency injection to your Lambda functions using syntax you are familiar with. When you add a [LambdaStartup] attribute to a Startup.cs file, the Lambda Annotations framework will generate the required code at compile time.

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

Your Lambda function can inject services using either constructor injection or by injecting into individual methods using the [FromServices] attribute.

[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); } }

Lambda function handler restrictions

Note that there are some restrictions on the handler signature.

  • It may not be unsafe and use pointer types in the handler signature, though you can use unsafe context inside the handler method and its dependencies. For more information, see unsafe (C# Reference) on the Microsoft Docs website.

  • It may not pass a variable number of parameters using the params keyword, or use ArgIterator as an input or a return parameter, which is used to support a variable number of parameters.

  • The handler may not be a generic method, for example, IList<T> Sort<T>(IList<T> input).

  • Async handlers with signature async void are not supported.

Code best practices for C# Lambda functions

Adhere to the guidelines in the following list to use best coding practices when building your Lambda functions:

  • Separate the Lambda handler from your core logic. This allows you to make a more unit-testable function.

  • Control the dependencies in your function's deployment package. The Amazon Lambda execution environment contains a number of libraries. To enable the latest set of features and security updates, Lambda will periodically update these libraries. These updates may introduce subtle changes to the behavior of your Lambda function. To have full control of the dependencies your function uses, package all of your dependencies with your deployment package.

  • Minimize the complexity of your dependencies. Prefer simpler frameworks that load quickly on execution environment startup.

  • Minimize your deployment package size to its runtime necessities. This will reduce the amount of time that it takes for your deployment package to be downloaded and unpacked ahead of invocation. For functions authored in .NET, avoid uploading the entire Amazon SDK library as part of your deployment package. Instead, selectively depend on the modules which pick up components of the SDK you need (e.g. DynamoDB, Amazon S3 SDK modules and Lambda core libraries).

  • Take advantage of execution environment reuse to improve the performance of your function. Initialize SDK clients and database connections outside of the function handler, and cache static assets locally in the /tmp directory. Subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time.

    To avoid potential data leaks across invocations, don’t use the execution environment to store user data, events, or other information with security implications. If your function relies on a mutable state that can’t be stored in memory within the handler, consider creating a separate function or separate versions of a function for each user.

  • Use a keep-alive directive to maintain persistent connections. Lambda purges idle connections over time. Attempting to reuse an idle connection when invoking a function will result in a connection error. To maintain your persistent connection, use the keep-alive directive associated with your runtime. For an example, see Reusing Connections with Keep-Alive in Node.js.

  • Use environment variables to pass operational parameters to your function. For example, if you are writing to an Amazon S3 bucket, instead of hard-coding the bucket name you are writing to, configure the bucket name as an environment variable.

  • Avoid using recursive code in your Lambda function, wherein the function automatically calls itself until some arbitrary criteria is met. This could lead to unintended volume of function invocations and escalated costs. If you do accidentally do so, set the function reserved concurrency to 0 immediately to throttle all invocations to the function, while you update the code.

  • Do not use non-documented, non-public APIs in your Lambda function code. For Amazon Lambda managed runtimes, Lambda periodically applies security and functional updates to Lambda's internal APIs. These internal API updates may be backwards-incompatible, leading to unintended consequences such as invocation failures if your function has a dependency on these non-public APIs. See the API reference for a list of publicly available APIs.

  • Write idempotent code. Writing idempotent code for your functions ensures that duplicate events are handled the same way. Your code should properly validate events and gracefully handle duplicate events. For more information, see How do I make my Lambda function idempotent?.