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.
This page describes how to work with Lambda function handlers in C# to work with the .NET managed runtime, including options for project setup, naming conventions, and best practices. This page also includes an example of a C# Lambda function that takes in information about an order, produces a text file receipt, and puts this file in an Amazon Simple Storage Service (S3) bucket. For information about how to deploy your function after writing it, see Build and deploy C# Lambda functions with .zip file archives or Deploy .NET Lambda functions with container images.
Setting up your C# handler project
When working with Lambda functions in C#, the process involves writing your code, then deploying your code to Lambda. There are two different execution models for deploying Lambda functions in .NET: the class library approach and the executable assembly approach.
In the class library approach, you package your function code as a .NET assembly (.dll
)
and deploy it to Lambda with the .NET managed runtime (dotnet8
). For the handler name,
Lambda expects a string in the format AssemblyName::Namespace.Classname::Methodname
.
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 the
top-level
statements featuredotnet8
). For the handler name, you
provide Lambda with the name of the executable assembly to run.
The main example on this page illustrates the class library approach. You can initialize your
C# Lambda project in various ways, but the easiest way is to use the .NET CLI with the
Amazon.Lambda.Tools
CLI. Set up the Amazon.Lambda.Tools
CLI by following
the steps in Setting up your .NET development environment. Then, initialize your project with the following
command:
dotnet new lambda.EmptyFunction --name ExampleCS
This command generates the following file structure:
/project-root └ src └ ExampleCS └ Function.cs (contains main handler) └ Readme.md └ aws-lambda-tools-defaults.json └ ExampleCS.csproj └ test └ ExampleCS.Tests └ FunctionTest.cs (contains main handler) └ ExampleCS.Tests.csproj
In this file structure, the main handler logic for your function resides in the
Function.cs
file.
Example C# Lambda function code
The following example C# Lambda function code takes in information about an order, produces a text file receipt, and puts this file in an Amazon S3 bucket.
Example Function.cs
Lambda function
using System;
using System.Text;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Amazon.S3;
using Amazon.S3.Model;
// Assembly attribute to enable Lambda function logging
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace ExampleLambda;
public class Order
{
public string OrderId { get; set; } = string.Empty;
public double Amount { get; set; }
public string Item { get; set; } = string.Empty;
}
public class OrderHandler
{
private static readonly AmazonS3Client s3Client = new();
public async Task<string> HandleRequest(Order order, ILambdaContext context)
{
try
{
string? bucketName = Environment.GetEnvironmentVariable("RECEIPT_BUCKET");
if (string.IsNullOrWhiteSpace(bucketName))
{
throw new ArgumentException("RECEIPT_BUCKET environment variable is not set");
}
string receiptContent = $"OrderID: {order.OrderId}\nAmount: ${order.Amount:F2}\nItem: {order.Item}";
string key = $"receipts/{order.OrderId}.txt";
await UploadReceiptToS3(bucketName, key, receiptContent);
context.Logger.LogInformation($"Successfully processed order {order.OrderId} and stored receipt in S3 bucket {bucketName}");
return "Success";
}
catch (Exception ex)
{
context.Logger.LogError($"Failed to process order: {ex.Message}");
throw;
}
}
private async Task UploadReceiptToS3(string bucketName, string key, string receiptContent)
{
try
{
var putRequest = new PutObjectRequest
{
BucketName = bucketName,
Key = key,
ContentBody = receiptContent,
ContentType = "text/plain"
};
await s3Client.PutObjectAsync(putRequest);
}
catch (AmazonS3Exception ex)
{
throw new Exception($"Failed to upload receipt to S3: {ex.Message}", ex);
}
}
}
This Function.cs
file contains the following sections of code:
-
using
statements: Use these to import C# classes that your Lambda function requires. -
[assembly: LambdaSerializer(...)]
:LambdaSerializer
is an assembly attribute that tells Lambda to automatically convert JSON event payloads into C# objects before passing them to your function. -
namespace ExampleLambda
: This defines the namespace. In C#, the namespace name doesn't have to match the filename. -
public class Order {...}
: This defines the shape of the expected input event. -
public class OrderHandler {...}
: This defines your C# class. Within it, you'll define the main handler method and any other helper methods. -
private static readonly AmazonS3Client s3Client = new();
: This initializes an Amazon S3 client with the default credential provider chain, outside of the main handler method. This causes Lambda to run this code during the initialization phase. -
public async ... HandleRequest (Order order, ILambdaContext context)
: This is the main handler method, which contains your main application logic. -
private async Task UploadReceiptToS3(...) {}
: This is a helper method that's referenced by the mainhandleRequest
handler method.
Because this function requires an Amazon S3 SDK client, you must add it to your project's dependencies.
You can do so by navigating to src/ExampleCS
and running the following command:
dotnet add package AWSSDK.S3
By default, the generated aws-lambda-tools-defaults.json
file doesn't contain
profile
or region
information for your function. In addition, update
the function-handler
string to the correct value
(ExampleCS::ExampleLambda.OrderHandler::HandleRequest
). You can manually make this
update and add the necessary metadata to use a specific credentials profile and region for your
function. For example, your aws-lambda-tools-defaults.json
file should look
similar to this:
{
"Information": [
"This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
"To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
"dotnet lambda help",
"All the command line options for the Lambda command can be specified in this file."
],
"profile": "default",
"region": "us-east-1",
"configuration": "Release",
"function-architecture": "x86_64",
"function-runtime": "dotnet8",
"function-memory-size": 512,
"function-timeout": 30,
"function-handler": "ExampleCS::ExampleLambda.OrderHandler::HandleRequest"
}
For this function to work properly, its execution role
must allow the s3:PutObject
action. If you're using the
dotnet lambda deploy-function
command (i.e. dotnet lambda deploy-function ExampleCS
),
the AWSLambdaExecute
policy in the CLI prompts contain the necessary permissions for you to
invoke this function successfully.
Also, ensure that
Finally, ensure that you define the RECEIPT_BUCKET
environment
variable. After a successful invocation, the Amazon S3 bucket should contain a receipt file.
Class library handlers
The main example code on this page illustrates a class library handler. Class library handlers have the following structure:
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace NAMESPACE;
...
public class CLASSNAME {
public async Task<string> METHODNAME (...) {
...
}
}
When you create a Lambda function, you need to provide Lambda with information about your function's
handler in the form of a string in the Handler field.
This tells Lambda which method in your code to run when your function is invoked. In C#, for class library
handlers, the format of the handler string is ASSEMBLY::TYPE::METHOD
, where:
-
ASSEMBLY
is the name of the .NET assembly file for your application. If you're using theAmazon.Lambda.Tools
CLI to build your application and you don't set the assembly name using theAssemblyName
property in the.csproj
file, thenASSEMBLY
is simply the name of your.csproj
file. -
TYPE
is the full name of the handler type, which isNAMESPACE.CLASSNAME
. -
METHOD
is the name of the main handler method in your code, which isMETHODNAME
.
For the main example code on this page, if the assembly is named ExampleCS
, then
the full handler string is ExampleCS::ExampleLambda.OrderHandler::HandleRequest
.
Executable assembly handlers
You can also define Lambda functions in C# as an executable assembly. Executable assembly handlers
utilize C#'s top-level statements feature, in which the compiler generates the Main()
method and puts your function code within it. When using executable assemblies, the Lambda runtime
must be bootstrapped. To do this, use the LambdaBootstrapBuilder.Create
method in your
code. The inputs to this method are the main handler function as well as the Lambda serializer to use.
The following shows an example of an executable assembly handler in C#:
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)
};
};
In the Handler field
for executable assembly handlers, the handler string that tells Lambda how to run your code is the name
of the assembly. In this example, that's GetProductHandler
.
Valid handler signatures for C# functions
In C#, valid Lambda handler signatures take between 0 and 2 arguments. Typically, your handler signature has two arguments, as shown in the main example:
public async Task<string> HandleRequest(Order order, ILambdaContext context)
When providing two argumenhts, the first argument must be the event input, and the second argument must be the Lambda context object. Both arguments are optional. For example, the following are also valid Lambda handler signatures in C#:
-
public async Task<string> HandleRequest()
-
public async Task<string> HandleRequest(Order order)
-
public async Task<string> HandleRequest(ILambdaContext context)
Apart from the base syntax of the handler signature, there are some additional restrictions:
-
You cannot use the
unsafe
keyword in the handler signature. However, you can use theunsafe
context inside the handler method and its dependencies. For more information, see unsafe (C# reference)on the Microsoft documentation website. -
The handler may not use the
params
keyword, or useArgIterator
as an input or return parameter. These keywords support a variable number of parameters. The maximum number of arguments your handler can accept is two. -
The handler may not be a generic method. In other words, it can't use generic type parameters such as
<T>
. -
Lambda doesn't support async handlers with
async void
in the signature.
Handler naming conventions
Lambda handlers in C# don't have strict naming restrictions. However, you must ensure that you provide the correct handler string to Lambda when you deploy your function. The right handler string depends on if you're deploying a class library handler or an executable assembly handler.
Although you can use any name for your handler, function names in C# are generally in
PascalCase. Also, although the file name doesn't need to match the class name or handler name,
it's generally a best practice to use a filename like OrderHandler.cs
if your class
name is OrderHandler
. For example, you can modify the file name in this example
from Function.cs
to OrderHandler.cs
.
Serialization in C# Lambda functions
JSON is the most common and standard input format for Lambda functions. In this example, the function expects an input similar to the following:
{
"orderId": "12345",
"amount": 199.99,
"item": "Wireless Headphones"
}
In C#, you can define the shape of the expected input event in a class. In this example, we
define the Order
class to model this input:
public class Order
{
public string OrderId { get; set; } = string.Empty;
public double Amount { get; set; }
public string Item { get; set; } = string.Empty;
}
If your Lambda function uses input or output types other than a Stream
object, you
must add a serialization library to your application. This lets you convert the JSON input into an
instance of the class that you defined. There are two methods of serialization for C# functions in
Lambda: reflection-based serialization and source-generated serialization.
Reflection-based serialization
AWS provides pre-built libraries that you can quickly add to your application. These
libraries implement serialization using
reflection
-
Amazon.Lambda.Serialization.SystemTextJson
– In the backend, this package usesSystem.Text.Json
to perform serialization tasks. -
Amazon.Lambda.Serialization.Json
– In the backend, this package usesNewtonsoft.Json
to perform serialization tasks.
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.
The main example on this page uses reflection-based serialization. Reflection-based serialization works out of the box with AWS Lambda and requires no additional setup, making it a good choice for simplicity. However, it does require more function memory usage. You may also see higher function latencies due to runtime reflection.
Source-generated serialization
With source-generated serialization, serialization code is generated at compile time. This removes the need for reflection and can improve the performance of your function. To use source-generated serialization in your function, you must do the following:
-
Create a new partial class that inherits from
JsonSerializerContext
, addingJsonSerializable
attributes for all types that require serialization or deserialization. -
Configure the
LambdaSerializer
to use aSourceGeneratorLambdaJsonSerializer<T>
. -
Update any manual serialization and deserialization in your application code to use the newly created class.
The following example shows how you can modify the main example on this page, which uses reflection-based serialization, to use source-generated serialization instead.
using System.Text.Json;
using System.Text.Json.Serialization;
...
public class Order
{
public string OrderId { get; set; } = string.Empty;
public double Amount { get; set; }
public string Item { get; set; } = string.Empty;
}
[JsonSerializable(typeof(Order))]
public partial class OrderJsonContext : JsonSerializerContext {}
public class OrderHandler
{
...
public async Task<string> HandleRequest(string input, ILambdaContext context)
{
var order = JsonSerializer.Deserialize(input, OrderJsonContext.Default.Order);
...
}
}
Source-generated serialization requires more setup than reflection-based serialization. However, functions using source-generated tend to use less memory and have better performance due to compile-time code generation. To help eliminate function cold starts, consider switching to source-generated serialization.
Note
If you want to use native ahead-of-time compilation (AOT) with Lambda, you must use source-generated serialization.
Accessing and using the Lambda context object
The Lambda context object contains information about
the invocation, function, and execution environment. In this example, the context object is of type
Amazon.Lambda.Core.ILambdaContext
, and is the second argument of the main handler
function.
public async Task<string> HandleRequest(Order order, ILambdaContext context) {
...
}
The context object is an optional input. For more information about valid accepted handler signatures, see Valid handler signatures for C# functions.
The context object is useful for producing function logs to Amazon CloudWatch. You can use the
context.getLogger()
method to get a LambdaLogger
object for logging.
In this example, we can use the logger to log an error message if processing fails for any reason:
context.Logger.LogError($"Failed to process order: {ex.Message}");
Outside of logging, you can also use the context object for function monitoring. For more information about the context object, see Using the Lambda context object to retrieve C# function information.
Using the AWS SDK for .NET v3 in your handler
Often, you'll use Lambda functions to interact with or make updates to other AWS resources. The simplest way to interface with these resources is to use the AWS SDK for .NET v3.
Note
The AWS SDK for .NET (v2) is deprecated. We recommend that you use only the AWS SDK for .NET v3.
You can add SDK dependencies to your project using the following Amazon.Lambda.Tools
command:
dotnet add package
<package_name>
For example, in the main example on this page, we need to use the Amazon S3 API to upload a receipt to S3. We can import the Amazon S3 SDK client with the following command:
dotnet add package AWSSDK.S3
This command adds the dependency to your project. You should also see a line similar to the
following in your project's .csproj
file:
<PackageReference Include="AWSSDK.S3" Version="3.7.2.18" />
Then, import the dependencies directly in your C# code:
using Amazon.S3;
using Amazon.S3.Model;
The example code then initializes an Amazon S3 client (using the default credential provider chain) as follows:
private static readonly AmazonS3Client s3Client = new();
In this example, we initialized our Amazon S3 client outside of the main handler function to avoid
having to initialize it every time we invoke our function. After you initialize your SDK client,
you can then use it to interact with other AWS services. The example code calls the Amazon S3
PutObject
API as follows:
var putRequest = new PutObjectRequest
{
BucketName = bucketName,
Key = key,
ContentBody = receiptContent,
ContentType = "text/plain"
};
await s3Client.PutObjectAsync(putRequest);
Accessing environment variables
In your handler code, you can reference any environment
variables by using the System.Environment.GetEnvironmentVariable
method. In this
example, we reference the defined RECEIPT_BUCKET
environment variable using the following
lines of code:
string? bucketName = Environment.GetEnvironmentVariable("RECEIPT_BUCKET");
if (string.IsNullOrWhiteSpace(bucketName))
{
throw new ArgumentException("RECEIPT_BUCKET environment variable is not set");
}
Using global state
Lambda runs your static code and the class constructor during the initialization phase before invoking your function for the first time. Resources created during initialization stay in memory between invocations, so you can avoid having to create them every time you invoke your function.
In the example code, the S3 client initialization code is outside the main handler method. The runtime initializes the client before the function handles its first event, which can lead to longer processing times. Subsequent events are much faster because Lambda doesn’t need to initialize the client again.
Simplify function code with the Lambda Annotations framework
Lambda Annotations
For an example of a full application utilizing Lambda Annotations, see the
PhotoAssetManagerawsdocs/aws-doc-sdk-examples
GitHub repository. The
main Function.cs
file in the PamApiAnnotations
directory uses
Lambda Annotations. For comparison, the PamApi
directory has equivalent files written
using the regular Lambda programming model.
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);
}
}
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 AWS 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 AWS 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 invocations in your Lambda function, where the function invokes itself or initiates a process that may invoke the function again. This could lead to unintended volume of function invocations and escalated costs. If you see an unintended volume of invocations, 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 AWS 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?
.