Creating an AWS Lambda function with .NET 6

Using a custom runtime

Andrew Craven
6 min readNov 26, 2021
.NET 6 Console app hosting a Lambda function

Overview

This is the first of three posts where I will walk you through my journey into the land of AWS Lambda. I will introduce a custom runtime I have created that allows you to deploy Lambda functions written using .NET 6, but also configure them using the Dependency Injection (DI) and Middleware patterns (which are no doubt familiar to most of you).

AWS say “You can implement an AWS Lambda runtime in any programming language. A runtime is a program that runs a Lambda function’s handler method when the function is invoked.”

I’ve created a package that can be referenced by a .NET 6 console app to bootstrap the Lambda function. The package is inspired, and in fact references, AWS’s Amazon.Lambda.RuntimeSupport package.

TL;DR;

The package can be found on NuGet; the custom runtime is Stackage.Aws.Lambda and there is also a dotnet new template using the custom runtime named Stackage.Aws.Lambda.DotNetNew.Templates. The template can be installed and used as follows:

dotnet new --install Stackage.Aws.Lambda.DotNetNew.Templates
dotnet new stackagelambda --name Your.Lambda.Function --framework net6.0

The source for these can be found in the concilify/stackage-aws-lambda-nuget and concilify/stackage-aws-lambda-dotnet-new repositories on GitHub which contains a couple of examples on how to use them to create and debug AWS Lambda functions. More information can be found in the README.md file of the repositories.

Background

I recently inherited a code-base containing multiple Lambda functions that use a bespoke framework. The framework appears to have been written due to lack of out-of-the-box support for Dependency Injection (DI) in the AWS Lambda runtime. The AWS Lambda runtime is the thing that bootstraps your AWS Lambda package and invokes the entry-point handlers.

The bespoke framework provides an interface which all the lambda handlers implement, which is resolved from a DI container (when the Lambda is invoked) allowing for each lambda handler to consume constructor injected dependencies. It also includes the entry-point handler (eg. a method with signature public returnType handler-name(inputType input, ILambdaContext context)) and proxies this to the resolved concrete lambda handler.

The first time the entry-point handler is invoked is known as a cold-start, where the assemblies are loaded up and any initialisation code is run. Subsequent invocations are known as warm-starts and result in much lower latency as the code has already been initialised. The AWS runtime keeps the instance warm for, I believe, an undefined period of time after it was last invoked. More information can be found online by searching for AWS Lambda cold-starts.

The bespoke framework provides some hooks for DI customisation that are executed the first time the entry-point handler is invoked during a cold-start and the resulting IServiceProvider is stored for the remaining life of the instance for subsequent warm-starts. From this point there are several steps that are performed before finally resolving the lambda handler from the IServiceProvider and invoking it.

The steps are effectively inline middleware; performing tasks such as payload and correlation ID parsing, setting logging scope properties, request and response logging and cancellation token handling of remaining time. The latter in particularly makes it hard to debug in an IDE, but generally it’s difficult to test drive changes to this behaviour as it’s all tightly coupled.

Those of you familiar with ASP.NET will no doubt have come across the concept of middleware. This is a pattern whereby a pipeline can be created by chaining together one or more middleware components. Each is able to perform some logic before and/or after the next middleware component, before ultimately invoking the desired logic at the end of the pipeline.

I’m aware Lambda functions can be created using ASP.NET Core (which would provide DI and middleware out-of-the-box), but that feels like a more heavyweight solution and would be a bigger refactor of the existing Lambda handlers towards controllers.

I wanted to see if I could improve this bespoke framework by applying the middleware pattern, removing the clumsy caching of the DI container by the entry-point handler and enabling easy debugging. What I ended up with was a completely new AWS Lambda runtime and a tool that locally emulates the AWS Lambda runtime API which handles communication between the custom runtime and the Lambda execution environment.

Prerequisites

As a minimum you will need the .NET Core SDK 3.1 installed, but the template used here also supports .NET Core SDK 5.0 and .NET SDK 6.0.

If you would like to build a Lambda function .zip package that can be deployed, you will need Docker installed. And if you would like to deploy that package, you will need an AWS account.

Creating a Lambda function

I’ve created a dotnet new template that creates a solution containing a skeleton Lambda function. This can be installed using dotnet new --install.

dotnet new --install Stackage.Aws.Lambda.DotNetNew.Templates

The skeleton solution can be created using dotnet new, replacing Your.Lambda.Function as appropriate. The supported frameworks are netcoreapp3.1, net5.0 and net6.0.

dotnet new slm --name Your.Lambda.Function --framework net6.0

If you want to go ahead and build this, run the build-package.ps1 script from the folder created. You will need Docker for this and may take a minute or so to run — it will create a Your.Lambda.Function.zip file when it’s complete.

You can create your Lambda function a number of ways, but I’m using the AWS Console. I’ve only updated the Function name and Runtime fields leaving the rest defaulted.

Clicking Create function will create a Hello World function which you can replace with the Your.Lambda.Function.zip file.

Clicking on the Upload from / .zip file button will open a popup to allow you to upload your .zip file.

Click Upload, select the Your.Lambda.Function.zip file and click Save. It will take a few moments to upload, but once it has click on the Test tab and update the body in the Test event.

Clicking the Test button will invoke the Lambda function. When it’s completed it will display the result; you can expand the Details to see the duration of the request and any logs it wrote.

This will have performed a cold-start since the instance wasn’t running. Clicking Test again will invoke the Lambda once more, but as it’s now running it will respond in less time — this is a warm-start.

You can also invoke your Lambda function using the AWS CLI; you may need to add the AWSLambdaRole policy to your cli user to assign the lambda:InvokeFunction permission. If you are using something other than Powershell, you may need to escape the quotes differently.

aws lambda invoke — function-name your-lambda-function — payload ‘{\”name\”: \”Andrew\”}’ — cli-binary-format raw-in-base64-out response.json

Summary

I’ve demonstrated how to create an AWS Lambda function using .NET 6. In future posts I will expand on this and show you how to debug the Lambda function locally and add middleware to the Lambda handler’s execution pipeline.

Follow me on Twitter @acraven_dev for notifications of new posts.

Credits

Inspiration from AWS’s Amazon.Lambda.RuntimeSupport package.

Thanks to Dave Cook and harry patel for their feedback on this post.

--

--