The Lambda Annotations 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, 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.
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.
[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
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.
"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.
"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
"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.
[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.
[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, showing the generated code.
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.
Top comments (0)