Home  > Resources  > Blog

Serverless HTTP API with AWS Lambda and C#

 
September 30, 2024 by Jason Bell
Category: AWS

Serverless computing is an attractive option for many cloud-based workloads. It provides the ability to pay for infrastructure on an as-used basis and avoid many of the tasks associated with server management. All the major cloud providers offer a serverless option – Google has “Could Run”, Microsoft has “Azure Functions”, and AWS has “AWS Lambda”. For each offering, there are a variety of programming languages that you can choose from. In this article, we build an HTTP API with AWS Lambda, .NET 8, and C#.

Use Cases for AWS Lambda

Since AWS Lambda is, at its core, simply an alternative way to execute code, it can be used to run just about any C# application or backend service. However, there are two primary use cases:

  • HTTP APIs that will be consumed by other applications or services
  • Code that executes in response to an event in your AWS account (file uploaded to S3, an update in DynamoDB, an SNS notification, etc.)

Although many of the steps are the same for both use cases, we will focus today on building an HTTP API that is available via a public HTTPS endpoint.

Getting Started with AWS

The first steps you should take before developing any solution for AWS are:

  1. Create an AWS account
  2. Configure a developer user in your AWS account
  3. Install and configure the AWS command line tools
  4. Install and configure the toolkit for your IDE

We will not cover these initial steps here but there is a blog post on the AWS site that goes through everything in detail. Feel free to head on over to Get started with .NET development on AWS and then come back here when you’re ready to go.

Getting Started with AWS Lambda

Now that you have your environment configured for working with AWS, you can install a couple of additional things for working with AWS Lambda. The first thing we’ll install is the Amazon.Lambda.Templates NuGet package. This package includes a collection of .NET project templates for AWS Lambda that address a variety of different use cases.

dotnet new install Amazon.Lambda.Templates

Once installed, the project templates will be available for you to use at the command line (with “dotnet new”) and in your IDE ([File > New Project…] in Visual Studio).

The second thing we will install is the .NET Lambda Global Tools extension. This extension provides command line tools that make it easy to package and deploy your code to AWS Lambda.

dotnet tool install -g Amazon.Lambda.Tools

If you installed one of the IDE toolkits, you also have a visual interface within your IDE that you can use to perform the same deployment tasks that we will perform at the command line.

Lambda Annotations Framework

To create an API in C# that will work when deployed to AWS Lambda, the API must be implemented using the AWS Lambda programming model – this includes a collection of custom Lambda data types and a different way for handling requests/responses. There are many project templates that provide a good starting point for writing an API in that way.

However, AWS has created something for .NET developers called the Lambda Annotations Framework. This is a NuGet package that you can add to a .NET project that will allow you to write an API in the normal .NET style (the same way you would if not using Lambda). When using the Annotations Framework, you decorate your API endpoints with certain attributes and the Annotations Framework will use code generation to transform your code to use the Lambda programming model. The serverless.Annotations project template provides a good starting point for using the Annotations Framework and we’ll use that in the next section to create our API.

Creating a New Project

Create a new project using the serverless.Annotations template (using the dotnet CLI or by selecting the template in your IDE) and name the project LambdaDemo.

dotnet new serverless.Annotations --output LambdaDemo

Template Description in Visual Studio

Before we modify any of the code, let’s look at some of the files created by the template. We’ll start by looking at the contents of LambdaDemo.csproj. The first few lines are normal for a .NET 8 library but then there is an element for AWS Lambda:

<AWSProjectType>Lambda</AWSProjectType>

This setting is what the AWS tooling uses to detect a project that is designed for AWS Lambda.

All the other settings are normal for .NET but with values that are optimized for AWS Lambda (with comments that describe why). Notice the four NuGet package references that are included:

  • Amazon.Lambda.Core – Interface and classes that are helpful when running in the Lambda environment (ILambdaContext, ILambdaLogger, etc.)
  • Amazon.Lambda.APIGatewayEvents – Types that are useful when using AWS API Gateway to expose your Lambda functions as an HTTP or REST API.
  • Amazon.Lambda.Serialization.SystemTextJson – Custom serializer used to serialize/deserialize .NET types in Lambda functions with the formatting policies that Lambda requires.
  • Amazon.Lambda.Annotations – The package we discussed earlier that will allow us to use .NET attributes to define our Lambda functions.

The next two files we want to look at are ICalculatorService.cs and CalculatorService.cs. Here is the code for those files (with the comments removed).

public interface ICalculatorService
{
  int Add(int x, int y);
  int Subtract(int x, int y);
  int Multiply(int x, int y);
  int Divide(int x, int y);
}

public class CalculatorService : ICalculatorService
{
  public int Add(int x, int y) { return x + y; }
  public int Subtract(int x, int y) { return x - y; }
  public int Multiply(int x, int y) { return x * y; }
  public int Divide(int x, int y) { return x / y; }
}

There is nothing special here but notice that neither of these files contain anything that has to do with AWS Lambda. This is simply a piece of functionality written in C# that we would like to expose to the world as an HTTP API by using Lambda. This class could easily be extended to query a database or implement other custom business logic. Having a separate interface defined will be helpful when configuring dependency injection (what we will look at next).

Dependency Injection

Our Lambda functions can use the same dependency injection system that many .NET developers are familiar with from building web applications and APIs with ASP.NET Core. There are just some minor adjustments necessary to use it with a Lambda function.

The first step is to identify a class that Lambda should use to register items with the DI system. This class can have any name but must be annotated with the [LambdaStartup] attribute. The template uses the fully-qualified name for this attribute but you could instead add a using directive for Amazon.Lambda.Annotations.

[LambdaStartup]
public class Startup {
  ...
}

A class that has this attribute should implement a method named ConfigureServices that uses the provided IServiceCollection to register items with the dependency injection system.

public void ConfigureServices(IServiceCollection services)

Since we are using the standard .NET DI system, all the various registration methods are available (a static factory method, AddDbContext for Entity framework, etc.). However, you should be aware of how running in Lambda’s execution environment affects the lifecycle of registered services. Let’s talk about that now…

Lifecycle of a Lambda Function

The first time a Lambda function is invoked, it will result in a “cold start”. During a cold start, Lambda creates a new isolated runtime environment, executes the ConfigureServices method (if you defined one), and begins the “invoke phase” to execute the Lambda function.

Once the execution of a Lambda function completes, the execution environment is frozen. You do not pay for compute time when a Lambda function is in this state. When another request arrives, the existing execution environment can be resumed, and this is referred to as a “warm start”. This process is much faster than a “cold start” and the ConfigureServices method is not executed.

Notes from the official AWS documentation…

  • “To improve resource management and performance, the Lambda service retains the [frozen] execution environment for a non-deterministic period of time.”
  • “When writing your Lambda function code, treat the execution environment as stateless, assuming that it only exists for a single invocation. Lambda terminates execution environments every few hours to allow for runtime updates and maintenance—even for functions that are invoked continuously.”

For dependency injection, this means that for a type registered as a singleton, a new object will only be created after a “cold start”. For a type registered as transient or scoped, a new object will be created every time the Lambda function is invoked (cold or warm).

In the demo project, the calculator service is registered as a singleton. This is appropriate for most cases (when following the official guidance and designing Lambda functions to be stateless).

services.AddSingleton(new CalculatorService());

Exposing C# Methods as Lambda Functions

In the demo project, the Functions class uses a constructor parameter to receive the calculator service via dependency injection and defines several methods that are annotated with the [LambdaFunction] and [HttpApi] attributes. Here is the code for the Add method:

[LambdaFunction()]
[HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
public int Add(int x, int y, ILambdaContext context)
{
  var sum = _calculatorService.Add(x, y);
  context.Logger.LogInformation($"{x} plus {y} is {sum}");
  return sum;
}

[LambdaFunction] indicates that this function should be exposed as a Lambda function and has several optional parameters including MemorySize and Timeout.

[HttpApi] configures a Lambda function to be called from an API Gateway HTTP API.

ILambdaContext provides information about the request and the current Lambda execution environment.

context.Logger is an implementation of ILogger that writes entries to AWS CloudWatch

We will leave the function as-is, but the [HttpApi] attribute (and API Gateway) is not required to expose a Lambda function via HTTP. Instead, you can configure a Lambda Function URL using the SDK or in the AWS Management Console.

The Lambda Annotations Framework will look for functions annotated with the [LambdaFunction] attribute and then automatically update the serverless.template file. For example, if you make a change to the name of the Add function now and then look at the serverless.template file, you will see that the change is reflected immediately.

"LambdaDemoFunctionsAddGenerated": {
  "Type": "AWS::Serverless::Function"
  },
  "Properties": {
    "Runtime": "dotnet8",
    "MemorySize": 512,
    "Timeout": 30,
    "PackageType": "Zip",
    "Handler": "LambdaDemo::LambdaDemo.Functions_Add_Generated::Add",
    "Events": {
      "RootGet": {
        "Type": "HttpApi",
        "Properties": {
          "Path": "/add/{x}/{y}",
          "Method": "GET"

Deployment

To prepare for deployment, you should specify an AWS region in the aws-lambda-tools-defaults.json file. For this example, I will use us-east-1.

"region": "us-east-1",

When ready to deploy, we can use the Lambda .NET CLI that we installed earlier (or by using the UI provided by one of the IDE toolkits). You need to be in the same directory as the serverless.template file when executing this command.

dotnet lambda deploy-serverless

The CLI tool will prompt you for any missing information that it needs to perform the deployment. Since this example uses API Gateway, you will be prompted for a CloudFormation Stack Name. For this example, I will use LambdaDemo.

Enter CloudFormation Stack Name: (CloudFormation stack name for an AWS Serverless application)
LambdaDemo

You will then be asked for the name of an S3 bucket for the build output. The deployment of the final package to AWS Lambda will be made from this S3 bucket. For this demo, I created an S3 bucket ahead of time with the default settings and a name of “lambdademo05”.

Enter S3 Bucket: (S3 bucket to upload the build output)
lambdademo05

If the deployment completes successfully, the tool will provide you with a URL that can be used to access your API. The URL will look something like the following:

https://z732nuqa61.execute-api.us-east-1.amazonaws.com/

We can test one of the calculator methods, by making an HTTP GET request with a web browser.

https://z561nuxa75.execute-api.us-east-1.amazonaws.com/add/3/4

Post-deployment, we can see the individual functions in the Lambda Dashboard.

AWS Lambda Dashboard

We can also see the functions grouped together as an HTTP API in the API Gateway dashboard.

AWS API Gateway Dashboard

Deleting a Deployment

The easiest way to delete a deployment (after doing a test like this) is to delete the Stack in the CloudFormation dashboard. The Stack will have the name that you provided when performing the deployment. Deleting the Stack will delete the API and all the individual Lambda functions, but it will not delete the contents of the S3 bucket.

Pricing

At the time of this writing, AWS offers a free tier for AWS Lambda that includes one million free requests per month and 400,000 GB-seconds of compute time. Beyond that, pricing in all of the US regions starts at $0.20 per 1 million requests. The cost for compute time will vary based on the amount of memory you allocate to a function. Detailed information about current pricing can be found at https://aws.amazon.com/lambda/pricing.

When looking at pricing, keep in mind that you might want to enhance the functionality of your Lambda functions by using additional services like Amazon API Gateway and each additional service will have its own pricing structure.

AWS Training and Upskilling for Organizations

Web Age Solutions offers in-person or online AWS Training for all roles, including architects, data analysts, security professionals, and AWS Training for Developers.

As an AWS training partner, we can offer official AWS certification training, helping you to prepare for the exams and earn certification.

About the Author

Jason Bell has been developing software of one kind or another since his first programs on a TI-99/4A in 1982. Jason has co-authored five books on software development, has been a technical trainer for over 20 years, and is the Chief Engineer at Treeloop, Inc.

Follow Us

Blog Categories