Building .Net Core applications using Amazon DynamoDB

Building .Net Core applications using Amazon DynamoDB

Amazon DynamoDB is a fully managed NoSQL database service that provides fast and predictable performance with seamless scalability.

In this post I am going to show how to build an application using Amazon DynamoDB. If you are not familiar with this technology, you can read more here

Prerequisites

We need to have an AWS account to be able to use the service. In order to set up Amazon DynamoDB the minimum steps would be:

  1. Create an account
  2. Install Amazon CLI on your computer
  3. Create Amazon DynamoDB table using Amazon CLI or the Web Console
  4. Create an AIM role for our application with read/write permissions for the table
  5. Set up the local AWS profile and credentials

To get more information please follow the Amazon developer guide  However, the demo application is using a docker version of DynamoDB. And we can run it without having access to the real service which is very convenient for testing and development purposes.  

We will also need the .Net Core 3.1 SDK and the Docker installed.

Developing a client application

We are going to build a simple Product Reviews Web API supporting the following use cases:

  • A user can post a product review including a product name, a rank (1-5)  
  • A user can get all his reviews
  • A user can get his review for a product
  • An administrator can retrieve all the reviews for all the users

An example of a review post by a user:

curl --location --request POST 'http://localhost:5000/productreviews/2' \
--header 'Content-Type: application/json' \
--data-raw '{
	"productName":"Apple iPhone 11",
	"rank": 5,
	"review": "The phone is awesome. I enjoy its camera a lot"
}'

Based on the current access patterns it's reasonable to have an Amazon DynamoDB table with the partition key UserId and the sort key ProductName. The combination of both represent the primary key.
Let's bootstrap a new web API application using dotnet new webapi and install a couple of dependencies:

<ItemGroup>
    <PackageReference Include="AWSSDK.DynamoDBv2" Version="3.3.105.33" />
    <PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.3.101" />
  </ItemGroup>

The first dependency is the Amazon SDK for DynamoDB. The second AWSSDK.Extensions.NETCore.Setup is the extensions package providing configiration extensions for .Net Core. We will configure the SDK as below:

services.AddAWSService<IAmazonDynamoDB>(Configuration.GetAWSOptions("DynamoDb"));

and adding the config section in the appsettings file:

"DynamoDb": {
    "ServiceURL": "http://localhost:8000/",
    "Region": "eu-west-2",
    "AWSProfileName": "product-reviews"
  }

In the settings ServiceURL is pointed to the local instance of DynamoDB. The local instance is provided by the used docker image (see below) The Region and the AWSProfileName are required to let the SDK know where to find your Amazon credentials and which region to use.

We will need to use a couple of docker images to be able to run Amazon DynamoDb locally, as shown in the docker-compose file:

services:
  dynamodb:
    image: cnadiminti/dynamodb-local
    container_name: dynamodb
    ports:
      - "8000:8000"
    networks:
      - dynamodb-network
    dynamodb-ui:
        image:  aaronshaf/dynamodb-admin
        container_name: dynamodb-ui
        ports:
          - "8001:8001"
        environment:
          - "DYNAMO_ENDPOINT=http://dynamodb:8000"
        networks:
          - dynamodb-network

When we have brought up the container using docker-compose up in the command line we should be able to open up the dynamodb-ui on http://localhost:8001 and create the table manually:
1-1

Now the setup should be ready and we can start implementing the API.

In this demo we are gong to use the Object Persistence Model provided by the SDK. The Amazon DynamoDB SDK also supports the Document Model and the Low Level Model which is out of scope of this post.

The object persistence model enables you to map your client-side classes to Amazon DynamoDB tables. It requires minimum user code.

This is how our ProductReviewItem class looks like representing a table record.

    [DynamoDBTable("ProductReview")]
    public class ProductReviewItem
    {
        [DynamoDBHashKey]
        public int UserId { get; set; }
        [DynamoDBRangeKey]
        public string ProductName { get; set; }
        public StarRank Rank { get; set; }
        public string Review { get; set; }
        public DateTime ReviewOn { get; set; }
    }

We have decorated the key properties and class with several attributes.
The application accesses the Amazon DynamoDB table via ProductReviewRepository implementation. The DynamoDBContext SDK instance is used to read and write from/to the table, for example:

public ProductReviewRepository(IAmazonDynamoDB dynamoDbClient)
        {
            if (dynamoDbClient == null) throw new ArgumentNullException(nameof(dynamoDbClient));
            _context = new DynamoDBContext(dynamoDbClient);
        }
        
        // Save a review
        public async Task AddAsync(ProductReviewItem reviewItem)
        {
            await _context.SaveAsync(reviewItem);
        }
        
        // Get a review by user id and product name
        public async Task<ProductReviewItem> GetReviewAsync(int userId, string productName)
        {
            return await _context.LoadAsync<ProductReviewItem>(userId, productName);
        }
        

All the mapping is being done behind the scenes by the Amazon DynamoDB SDK. The object persistence model is a hight-level model and requires minimum user code.

In Order to query data there are two ways of doing this:

  1. ScanAsync<T>()
  2. QueryAsync<T>()

The ScanAsync<T> is expensive in terms of the cost and the time. You should use it as less as possible. The QueryAsync<T> allows to query data using the table indexes. This is the most efficient way. So it's important to index your data according to the query patterns. Get All and Get by Id examples:

public async Task<IEnumerable<ProductReviewItem>> GetAllAsync()
        {
            return await _context.ScanAsync<ProductReviewItem>(new List<ScanCondition>()).GetRemainingAsync();
        }

        public async Task<IEnumerable<ProductReviewItem>> GetUserReviewsAsync(int userId)
        {
            return await _context.QueryAsync<ProductReviewItem>(userId).GetRemainingAsync();
        }

In this application when a controller receives a request it gets passed to the ProductReviewService. This service maps the request to the ProductReviewItem and calls the repository to store the item in the Amazon DynamoDB table:

(Local Dynamo DB Table content):

2

Assuming you have set the real Amazon DynamoDB table as mentioned above, you can point out your application to this table by commenting out the line:
//"ServiceURL": "http://localhost:8000/",

Given we have added several reviews we can call the get all reviews endpoint which would scan the table and return everything back:
curl --location --request GET 'http://localhost:5000/productreviews'

[
    {
        "productName": "Apple iPhone 11",
        "rank": 5,
        "review": "The phone is awesome. I enjoy its camera a lot",
        "reviewOn": "2020-04-26T14:50:42.348+01:00",
        "userId": 2
    },
    {
        "productName": "Office Chair",
        "rank": 4,
        "review": 

You can find the project source code on my GitHub

Summary

In this post I have demonstrated  how to build .Net Core applications working with Amazon DynamoDB using the Object Persistence Model. The sample application was configured to work with the local DynamoDB service which is very important for testing and development reasons. At the same time it can be easily switched to work with the real service. Amazon DynamoDB is a very good piece of technology.