The Quickest Way To Build Your APIs In AWS Lambda Functions Using .NET
Tags: dotnet, aws, lambda
Published: 2024-02-06 05:16:42

The [Lambda Annotations Framework](https://www.rahulpnath.com/blog/aws-lambda-annotation-framework/) is a programming model that makes it easier to build AWS Lambda Functions using .NET.

The framework uses C# custom attributes and Source Generators to translate annotated Lambda functions to the regular Lambda programming model.

In a [previous post](https://www.rahulpnath.com/blog/aws-lambda-annotation-framework/), we learned how to get started with using the Annotations Framework and how it compares to using the default Lambda Programming model. The Annotations Framework removes boilerplate code required to integrate with API Gateway.

{% embed https://www.youtube.com/watch?v=Zx6JpVKUoLQ %}

In this post, let’s learn how to set up a basic CRUD (Create, Read, Update, and Delete) Order API Endpoint using the Lambda Annotation Framework.

We will use DynamoDB as our data store for this example and we will also learn how to set up the DynamoDB Table and Role Permissions via the same CloudFormation template that the Annotations framework generates.

## Create and Update API Endpoint

Let's first create the Create Endpoint for our API.

Using the Annotations Framework, we can use the `LambdaFunction` and `HttpApi` attribute to decorate a .NET function to be a Lambda Entry point.

The endpoints accept an object of type `Order` which represents an Order in our domain.

We have a simple `Order` class with the `OrderId` as it's primary key.

To bind the `Order` item from the requests body, we can use the `FromBody` attribute on the function parameter. This is very similar to how we do when building ASP.NET Core APIs.

> ⚠️ _Make sure the template path does not have a leading slash on it._

We also have specified a custom IAM Role Name as part of the `Role` parameter in the `LambdaFunction` attribute. More on this later.

```csharp
[LambdaFunction(Role = "@OrdersApiLambdaExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, template: "/orders")]
public async Task PostOrder([FromBody] Order order, ILambdaContext context)
{
    await _dynamodbContext.SaveAsync(order);
}

public class Order
{
    public string OrderId { get; set; }
    public decimal Total { get; set; }
    public DateTime CreatedDate { get; set; }
}
```

Using the `DynamoDBContext` we can save the `Order` into the table. If you are new to DynamoDB I highly recommend checking the below blog post to get started.

[Amazon DynamoDB For The .NET Developer](https://www.rahulpnath.com/blog/amazon-dynamodb-dotnet-developer)

For the DynamoDBContext save to work, we need to make sure the Table is set up and the Lambda Function has appropriate access to the table.

## Setting up DynamoDB Table and IAM Roles via CloudFormation

As part of the Lambda Annotations Framework, it auto-generates a `serverless.template` CloudFormation template file.

However, you can manually add Resources to the template file.

In our case, we need to set up the DynamoDB Table and the Lambda Access Roles with the appropriate permission to talk to DynamoDB Table.

To set up the Order DynamoDB table, let's add the below to the Resources section of the template.

```json
"OrderTable": {
  "Type": "AWS::DynamoDB::Table",
  "Properties": {
    "AttributeDefinitions": [
      {
        "AttributeName": "OrderId",
        "AttributeType": "S"
      }
    ],
    "KeySchema": [
      {
        "AttributeName": "OrderId",
        "KeyType": "HASH"
      }
    ],
    "TableName": "Order",
    "BillingMode": "PAY_PER_REQUEST"
  }
}
```

It sets up the DynamoDB table with a specific name and also the Keys for the table.

For setting up the Lambda IAM Role, let's use the below template in the Resources section.

It sets up full access to DynamoDB Order Table. Note I am using the CloudFormation function `Fn::GetAtt` to get the ARN of the DynamoDB Table created in the same template file.

```json
"OrdersApiLambdaExecutionRole": {
  "Type": "AWS::IAM::Role",
  "Properties": {
    "AssumeRolePolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Service": "lambda.amazonaws.com"
          },
          "Action": "sts:AssumeRole"
        }
      ]
    },
    "ManagedPolicyArns": [
      "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
    ],
    "Policies": [
      {
        "PolicyName": "OrderApiDynamoDBAccessPolicy",
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "dynamodb:*",
              "Resource": {
                "Fn::GetAtt": ["OrderTable", "Arn"]
              }
            }
          ]
        }
      }
    ]
  }
}
```

The `LambdaFunction` attribute specifies the `Role` property to use the new IAM role that we just created above.

We use the name `Role = "@OrdersApiLambdaExecutionRole"` to refer to the IAM role created in the template file. The `@` prefix refers to look up for the name from the CloudFormation template.

This generates the below section to the CloudFormation template for the Lambda Function

```json
"LambdaAnnotationSampleOrderApiFunctionPostOrderGenerated": {
  "Type": "AWS::Serverless::Function",
  "Metadata": {
    "Tool": "Amazon.Lambda.Annotations",
    "SyncedEvents": [
      "RootPost"
    ]
  },
  "Properties": {
    "Runtime": "dotnet6",
    "CodeUri": ".",
    "MemorySize": 256,
    "Timeout": 30,
    "Role": {
      "Fn::GetAtt": [
        "OrdersApiLambdaExecutionRole",
        "Arn"
      ]
    },
    "PackageType": "Zip",
    "Handler": "LambdaAnnotationSample.OrderApi::LambdaAnnotationSample.OrderApi.Function_PostOrder_Generated::PostOrder",
    "Events": {
      "RootPost": {
        "Type": "HttpApi",
        "Properties": {
          "Path": "/order",
          "Method": "POST"
        }
      }
    }
  }
}
```

You can deploy the template from the IDE or using the AWS CLI, which will create the Lambda Function, the IAM Role, the API Endpoint and wire up the integration for the route to the Lambda Function on the API Gateway.

## Read API Endpoint

To set up the Read API Endpoint, all we need to do is create another Function in the same class and add the `LambdaFunction` and `HttpApi` attribute. Since this is a GET, it specifies the appropriate `LambdaHttpMethod` on it.

```csharp
[LambdaFunction(Role = "@OrdersApiLambdaExecutionRole")]
[HttpApi(LambdaHttpMethod.Get, "/orders/{orderId}")]
public async Task<Order> GetOrder(string orderId, ILambdaContext context)
{
    return await _dynamodbContext.LoadAsync<Order>(orderId);
}
```

The function takes in the `orderId` as the parameter, which is bound from the path parameter in the request URL.

You can use the `DynamoDBContext` to retrieve the item from the Table and return the Order item.

Lambda Annotations will automatically convert this into JSON representation and send it as part of the HTTP response body.

## Delete API Endpoint

Very similar to the Get API Endpoint, we can add a new Function for the Delete endpoint.

```csharp
[LambdaFunction(Role = "@OrdersApiLambdaExecutionRole")]
[HttpApi(LambdaHttpMethod.Delete, "/orders/{orderId}")]
public async Task DeleteOrder(string orderId, ILambdaContext context)
{
    await _dynamodbContext.DeleteAsync<Order>(orderId);
}
```

It takes the `orderId` from the Path parameter and uses that to delete the item from the DynamoDB table.

## Annotations Framework Generated Code

The Lambda Annotations Framework uses .NET Source Generators to generate the boilerplate code to interact with the API Gateway.

Below is a screenshot of the DLL from [JetBrains dotPeek application](https://www.jetbrains.com/decompiler/whatsnew/?ref=rahulpnath.com), showing the generated code.


![Annotation Framework auto generated code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/43q809241c1wlp42xv9b.png)

Lambda Annotations Framework generated code using Source Generator.

As you can see, for each of the functions attributed with `LambdaFunction` it creates a new class, that wraps around the original function and adds the boilerplate code to talk with API Gateway.

This makes it easier for us developers to focus just on the business logic and not worry about how to integrate with the API Gateway.

You can find the full code sample [here](https://github.com/rahulpnath/youtube-samples/tree/main/LambdaAnnotationSample/LambdaAnnotationSample.OrderApi).