Quick Links

AWS Lambda Functions are a serverless computing model that lets you run code without servers. These are commonly written with languages like JavaScript and Python, but AWS now supports many different runtimes, including .NET for C#.

Why Use .NET For Lambda?

There are many different languages available for Lambda now, so you have a lot of options. Generally, JavaScript and Python are used for simple automation function that care about fast startup times. But, they're not the most performant for heavy processing, and being dynamically typed scripting languages is a major downside for complex applications.

If C# is your language of choice, there's not much downside to using it for Lambda, especially if switching to Python or JavaScript is too big of a hassle. The tooling AWS provides is nice as well, and you have access to the entire AWS SDK, meaning you can perform lookups to services like Lambda and DynamoDB with ease.

Also, AWS supports the entire .NET runtime, which means you can use other languages besides C# that also compile to .NET binaries. C# is overwhelmingly the most popular, but you could also write Lambda Functions in F# or VB.NET.

How Does It Perform?

Languages like Java and C# are generally much nicer, but there is a downside to using them. They both are compiled to bytecode that must be compiled at startup, so they have higher startup times, especially when starting cold. "Cold starts" are when AWS hasn't run the function in the last few minutes, so it won't have it cached, and will need to perform the just-in-time compilation again to get it up and running. This process can cause your functions to take a second or more to respond, which isn't good for web applications.

However, this problem is largely mitigated if you're using Lambda very often. You can also reduce cold start times entirely with provisioned concurrency. The regular response times for .NET are very high, and the performance is on par with the fully compiled languages like Go and Rust.

If you're currently using Java for Lambda functions, C# can be a viable replacement, as the modern .NET 6 runtime uses less memory and starts up quicker than the JVM in most cases.

Setting Up C# Lambda Functions

First, you will need .NET installed. AWS supports .NET Core 3.1 and .NET 6, so either of those two runtimes will work, but most importantly you will need the

        dotnet
    

 CLI installed so that you can install the Lambda templates. You can get .NET from Microsoft's documentation portal.

You'll need to install the Lambda templates, and the global Lambda tools.

dotnet new -i Amazon.Lambda.Templates

dotnet tool install -g Amazon.Lambda.Tools

There are a lot of options this installs; you can list them all with:

dotnet new --list

This tooling is quite nice, as it comes with many packaged templates preconfigured for different use cases. You will generally want one function per project to keep build sizes small, but you can have multiple functions in one DLL if you use AWS's Serverless templates, which deploy using CloudFormation templates. These are a lot more complicated to manage, so only use them if you're benefiting from it.

With .NET's Solution files though, you can have multiple projects side-by-side referencing common assemblies, so this isn't much of an issue.

For now, we'll go with the simple Empty Function template, which generates a project using .NET 6. You can create this from the command line, or from your editor's new project screen.

dotnet new lambda.EmptyFunction --name SimpleLambdaFunction --profile default --region us-east-1

This generates a very simple function---it takes a string as input, and is also passed an ILambdaContext. This is the Main() entry-point function for your Lambda and will be called by the runtime whenever the Lambda Function is invoked. This particular function returns a string, but you can also make it async and return a Task<string?>.

At the top you'll see an assembly attribute configuring a JSON Serializer. Internally, Lambda will handle deserializing the input content for you, and then will call your function. Afterwards, if it returns something, it will be written to the response stream. The Lambda libraries handle this boilerplate for you, and the code that wraps your function is in HandlerWrapper.

Essentially, it will handle all kinds of method signatures, and if your function takes an input, it will deserialize that input for you. If your function returns an output, it will serialize that output for you. You actually don't have to do any of this, as you can write functions that operate on raw Stream classes, but this is a nice wrapper class to make things easier.

What this means is that you are free to define your own models for inputs and outputs passed to and from the function, one of the nice perks of handling JSON with C#.

In this function, it deserializes the InputModel class, waits asynchronously for a second, and then returns an OutputModel class. This class is serialized back into the output stream so Lambda can handle it.

Running Lambda Functions

Running the function once you've made it is quite simple, as the Lambda .NET CLI provides a method for deploying it. Simply run deploy-function with

dotnet lambda deploy-function SimpleNETFunction

You will need to select an IAM role or create a new one, and you may need to add permissions to this new role. You should now see the function in your console:

Lambda provides a built-in tester which you can pass JSON to.

This will execute and show you all the details about the execution. In this case, with a very small minimal function, the cold startup time was less than 500ms, which is pretty decent for .NET and for Lambda in general. Once it's warm, the billed duration goes down to only a few milliseconds.

In this case, this function didn't use much memory at all, and bumping the function down to 128MB caused no problems.