fbpx
Hero Illustration
0 Comments
Mitrais, Software Development

Developing Serverless Applications using C# in AWS

What is Serverless?

Serverless is a cloud-native development model where developers can build and run applications without having to manage servers. Although servers are still required, the term “serverless” refers to the fact that these servers are fully managed, eliminating the need for manual management.

Serverless Services on AWS

AWS provides a range of services for deploying applications, including Database Services, Virtual Machines, and serverless services, which will be discussed in this blog post.

The serverless services that AWS offers include AWS Lambda, AWS Fargate, Amazon DynamoDB, AWS AppSync, and Amazon Aurora Serverless. If you are interested in exploring more serverless options, their website provides additional information. In this blog post, we will focus on developing a simple note app using AWS Lambda.

Application Programming Interface (API)

According to AWS, an Application Programming Interface (API) allows two software components to communicate using a set of definitions and protocols. For example, if we have a website to create notes and we want to store the data in the database but not expose the database to the public, we need to create an API as the interface between the website and the database.

Building a Simple Note App API in AWS Lambda

Preparation

Before developing the app, make sure to do the following steps:

Application Architecture

It is essential to have a comprehensive understanding of the overall solution before starting the development. Please see the picture below to gain insight into the application’s architecture.

We will have multiple AWS Lambda Functions, and we will expose each Lambda Function with an endpoint. Usually, the User will interact with an API Gateway instead of interacting directly with the Lambda Function. For this solution, we will expose the Lambda Function using Lambda Function URLs.

Preparing Amazon DynamoDB

We will use Amazon DynamoDB to store our notes data. Before we create the Amazon DynamoDB table, we recommend reading the documentation to have a full understanding of how to set up the Amazon DynamoDB. Let’s go back to our setup.

  1. We need to log into our AWS account and select the region where we will create the Amazon DynamoDB. In this case, we will use “ap-southeast-1 (Singapore)”.
  2. Open Amazon DynamoDB in the AWS Console and click the “Create table” button.
  3. Complete the table name and partition key and leave the other options as default. Then, fill in the fields as follows:
    • Table name: note
    • Partition key: id
  4. Click the “Create table” button at the bottom of the page.

Creating a CreateNote Lambda Function

We will begin coding our first Lambda Function and deploy it directly to AWS. We will use AWS Lambda Template to generate our project.

Step 1. Install the AWS Lambda template.

dotnet new -i Amazon.Lambda.Templates

This command installs the Amazon.Lambda.Templates packages, which contain project templates for creating .NET applications that can run on AWS Lambda.

Step 2. Generate the project.

dotnet new lambda.EmptyFunction -o CreateNote

This command generates a new .NET project with a template for the AWS Lambda function. The –o or –output options specify the name of the output directory, which in this case is “CreateNote”.

Step 3. Change the working directory to “CreateNote/src/CreateNote”.

cd CreateNote/src/CreateNote

Step 4. Add a library AWS SDK for DynamoDB.

dotnet add package AWSSDK.DynamoDBv2

This command adds the package AWSSDK.DynamoDBv2 to the .NET project. We will use this library to connect with DynamoDB.

Step 5. Add a library Lambda API Gateway Events.

dotnet add package Amazon.Lambda.APIGatewayEvents

This command adds the package Amazon.Lambda.APIGatewayEvents to the .NET project. We will use this library to recognize the request.

Step 6. Add “Note.cs” under the Models directory by copying and pasting the code below.

using System.ComponentModel.DataAnnotations;

namespace CreateNote.Models;

public class Note
{
    public string Message { get; set; } = string.Empty;
}

Step 7. Update the “Function.cs” file by copying and pasting the code below.

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using CreateNote.Models;
using System.Text.Json;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace CreateNote;

public class Function
{
    AmazonDynamoDBClient = new AmazonDynamoDBClient();
    /// <summary>
    /// A simple function to store note data into DynamoDB
    /// </summary>
    /// <param name="request"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task<string> CreateNoteAsync(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
    {
        var noteRequest = JsonSerializer.Deserialize<Note>(request.Body);
        var noteId = Guid.NewGuid().ToString();
        var noteItem = new Dictionary<string, AttributeValue>()
        {
            ["message"] = new AttributeValue { S = noteRequest!.Message },
            ["id"] = new AttributeValue { S = noteId }
        };
        var createRequest = new PutItemRequest
        {
            TableName = "note",
            Item = noteItem
        };
        var response = await amazonDynamoDBClient.PutItemAsync(createRequest);
        return response.HttpStatusCode == System.Net.HttpStatusCode.OK ? noteId : string.Empty;
    }
}

Step 8. Update the”aws-lambda-tools-defaults.json” file so that it looks like this code. Please note that we would need to update the region to be the same as the DynamoDB region.

{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "profile": "",
  "region": "ap-southeast-1",
  "configuration": "Release",
  "function-architecture": "x86_64",
  "function-runtime": "dotnet6",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "CreateNote::CreateNote.Function::CreateNoteAsync"
}

Lambda Function Deployment

There are two options for deploying Lambda Functions, and you can see this page to learn more about it. For this example, we will use .NET CLI.

Step 1. Install the Lambda Tools to deploy the Lambda Function.

dotnet tool install -g Amazon.Lambda.Tools

Step 2. Set up the IAM Role that has access to DynamoDB. We may use this IAM Policy as our role. We may also use this command to deploy and remember to update the function role name with our own role name.

dotnet lambda deploy-function CreateNote –function-role iam_for_lambda

Step 3. If the Lambda Function is deployed correctly, we will see this Lambda Function on the Lambda Dashboard page.

We will now enable the Function URLs for CreateNote.

  • Click “CreateNote” and navigate to the Configuration tab.
  • Click the “Function URL” side menu.
  • Click the “Create function URL” button.
  • Click the “NONE” radio button under Auth type. We will allow anonymous requests, and for best practice, we will protect our API using IAM.
  • Tick “Configure cross-origin resource sharing” and let “*” as the Allow origin.
  • Under Allow methods, choose POST.

Now, our API is ready. We can check the URL in the Function URL section or the Function Overview.

We can try the API using Postman, curl, or any other of our favorite tools. For example, we can use Postman to test our API.

Creating an UpdateNote Lambda Function

How do we update the note data? We must create an UpdateNote function that will look very similar to the CreateNote Lambda Function. We will highlight the differences.

Step 1. Generate the project.

dotnet new lambda.EmptyFunction -o UpdateNote

Step 2. Change the working directory to “UpdateNote/src/UpdateNote”.

cd UpdateNote/src/UpdateNote

Step 3. Add a library AWS SDK for DynamoDB.

Step 4. Add a library Lambda API Gateway Events.

Step 5. Add “Note.cs” under the Models directory by copying and pasting the code below.

namespace UpdateNote.Models;

public class Note
{
    public Guid Id { get; set; }
    public string Message { get; set; } = string.Empty;
}

Step 6. Update “Function.cs” by copying and pasting the code below.

using System.Text.Json;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using UpdateNote.Models;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace UpdateNote;

public class Function
{
    AmazonDynamoDBClient amazonDynamoDBClient = new AmazonDynamoDBClient();
    /// <summary>
    /// A simple function to update note data in DynamoDB
    /// </summary>
    /// <param name="request"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task<bool> UpdateNoteAsync(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
    {
        var noteRequest = JsonSerializer.Deserialize<Note>(request.Body);
        var noteItem = new Dictionary<string, AttributeValue>()
        {
            ["message"] = new AttributeValue { S = noteRequest!.Message },
            ["id"] = new AttributeValue { S = noteRequest.Id.ToString() }
        };
        var updateRequest = new PutItemRequest
        {
            TableName = "note",
            Item = noteItem
        };
        var response = await amazonDynamoDBClient.PutItemAsync(updateRequest);
        return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
    }
}

Step 7. Update the “aws-lambda-tools-defaults.json” file with the following code.

{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "profile": "",
  "region": "ap-southeast-1",
  "configuration": "Release",
  "function-architecture": "x86_64",
  "function-runtime": "dotnet6",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "UpdateNote::UpdateNote.Function::UpdateNoteAsync"
}

Step 8. Deploy our Lambda Function.

dotnet lambda deploy-function UpdateNote –function-role iam_for_lambda

Step 9. Create a Function URL with the following configurations.

Auth type: NONE

Configure cross-origin resource sharing: Enable

Allow origin: *

Allow methods: PATCH

Step 10. Test our API again. We will see the image below.

If we are unsure if it’s already updated, we can check on the DynamoDB page directly.

Creating a DeleteNote Lambda Function

For the DeleteNote function, we only need an id as the request. The steps to build the DeleteNote function are as follows.

Step 1. Generate the project.

dotnet new lambda.EmptyFunction -o DeleteNote

Step 2. Change the working directory to “DeleteNote/src/DeleteNote”.

cd DeleteNote/src/DeleteNote

Step 3. Add a library AWS SDK for DynamoDB.

Step 4. Add a library Lambda API Gateway Events.

Step 5. Update “Function.cs” by using the code below.

using System.Text.Json;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace DeleteNote;

public class Function
{
    AmazonDynamoDBClient = new AmazonDynamoDBClient();
  
    /// <summary>
    /// A simple function that delete a note from DynamoDB
    /// </summary>
    /// <param name="request"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task<bool> DeleteNoteAsync(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
    {
        var dataRequest = JsonSerializer.Deserialize<Dictionary<string, string>>(request.Body);
        var item = new Dictionary<string, AttributeValue>()
        {
            ["id"] = new AttributeValue(dataRequest!["Id"])
        };
        var response = await amazonDynamoDBClient.DeleteItemAsync("note", item);
        return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
    }
}

Step 6. Update the “aws-lambda-tools-defaults.json” file with the following code.

{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "profile": "",
  "region": "ap-southeast-1",
  "configuration": "Release",
  "function-architecture": "x86_64",
  "function-runtime": "dotnet6",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "DeleteNote::DeleteNote.Function::DeleteNoteAsync"
}

Step 7. Deploy our Lambda Function.

dotnet lambda deploy-function DeleteNote –function-role iam_for_lambda

Step 8. Create a Function URL with the following configurations.

Auth type: NONE

Configure cross-origin resource sharing: Enable

Allow origin: *

Allow methods: DELETE

Step 9. Test our API and we will see the image below.

Create a ListNote Lambda Function

After finishing the DeleteNote function, let’s move on to the ListNote function. We will use the scan operation, but we recommend that you do not use it for getting all of the data with one fetch. We recommend using pagination. Let’s go step by step.

Step 1. Generate the project.

dotnet new lambda.EmptyFunction -o ListNote

Step 2. Add a library AWS SDK for DynamoDB.

Step 3. Add a library Lambda API Gateway Events.

Step 4. Add “Note.cs” under the Models directory by using the code below.

namespace ListNote.Models;

public class Note
{
    public Guid Id { get; set; }
    public string Message { get; set; } = string.Empty;
}

Step 5. Update “Function.cs” by using the code below.

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using ListNote.Models;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace ListNote;

public class Function
{
    AmazonDynamoDBClient = new AmazonDynamoDBClient();
    /// <summary>
    /// A simple function that takes a string and does a ToUpper
    /// </summary>
    /// <param name="input"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task<List<Note>> ListNoteAsync(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
    {
        var dataRequest = request.QueryStringParameters ?? new Dictionary<string, string>();
        var specificId = dataRequest.ContainsKey("id");
        var queryRequest = new ScanRequest()
        {
            TableName = "note",
        };
        if (specificId)
        {
            queryRequest.FilterExpression = "id = :v_id";
            queryRequest.ExpressionAttributeValues = new Dictionary<string, AttributeValue>()
            {
                {":v_id", new AttributeValue() { S = dataRequest["id"] }}
            };
        }


        var response = await amazonDynamoDBClient.ScanAsync(queryRequest);
        return response.Items.Select(item => new Note()
        {
            Id = Guid.Parse(item["id"].S),
            Message = item["message"].S
        }).ToList();
    }
}

Step 6. Update the “aws-lambda-tools-defaults.json” file with the following code.

{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "profile": "",
  "region": "ap-southeast-1",
  "configuration": "Release",
  "function-architecture": "x86_64",
  "function-runtime": "dotnet6",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "ListNote::ListNote.Function::ListNoteAsync"
}

Step 7. Deploy our Lambda Function by referring to the CreateNote function for the command.

dotnet lambda deploy-function ListNote –function-role iam_for_lambda

Step 8. Create a Function URL with the following configurations.

Auth type: NONE

Configure cross-origin resource sharing: Enable

Allow origin: *

Allow methods: GET

Step 9. Test our API, and we will see the image below.

To explore more about the code, you can visit this Github Repository.

Alternative Serverless Solutions

AWS App Runner and AWS Lightsail are both cloud-based services offered by Amazon Web Services.

AWS App Runner is a fully managed container application service that lets us build, deploy, and run containerized web applications and API services without prior infrastructure or container experience. It will be more beneficial if we have containerized web applications. If you are unsure what containerization is, we suggest checking this website for your reference.

On the other hand, AWS Lightsail has some managed services, such as virtual private server (VPS) instances, containers, storage, and databases. All of the AWS Lightsail services are pre-configured by AWS. AWS Lightsail has a different pricing model where we need to pay a fixed monthly price for the resources that we use. For the serverless solution, we can use container services, storage, and databases.

In summary, AWS App Runner is a serverless solution with a containerized base. AWS Lightsail, on the other hand, is a pre-configured service that will be a good candidate for serverless solutions, especially container services.

Why use Serverless?

In line with what was stated by Cloudflare, Serverless is an architecture in which a vendor provides backend services as they are needed. The advantage of Serverless is that we only need to focus on our application or our code. We can deliver faster because we don’t need to worry about provisioning and managing servers. After reading this blog, we can confidently say that we feel even more inclined to use Serverless for our future projects.

Comparing Serverless Solutions

Here is a summary of the serverless solutions discussed.

TopicsAWS LambdaAWS App RunnerAWS Lightsail
Ability to use containerYesYesYes
Available to use HTTPS connectionYes (Function URL or Amazon API Gateway)YesYes
BillingEach execution of the LambdaEach running containersEach running application (fixed monthly price)
Region AvailabilityAlmost all regionsLimited regionsAlmost all regions

Based on the comparison, we recommend using AWS Lambda for simple functions that are rarely called. AWS Lambda is billed per execution and by execution time. In addition, it would be helpful to remember that AWS Lambda has a 15-minute time limit for each request. Consider using another computing service if you need more time to perform a function.

You might need to consider AWS App Runner if you have a more complex web application that requires a longer execution time. Also, be aware that AWS App Runner has limited region availability. You might try Amazon ECS with Fargate as your computing service if you need an architecture with orchestration, using containers, and serverless.

Author: Bervianto Leo Pratama, Software Engineer Analyst Programmer

Contact us to learn more!

Please complete the brief information below and we will follow up shortly.

    ** All fields are required
    Leave a comment