DynamoDB

We will be illustrating the basics of a Global Secondary Index(GSI) and a Local Secondary Index(LSI). This article is designed to illustrate the creation of 2 DynamoDB Tables(one with a single Primary Key, and another with a Composite Primary Key) with actual code that can be deployed. This will be done with the Cloud Development Kit(CDK) to create and deploy the required infrastructure as code(IAC). Our Lambdas will be written in C# DotNet to test out the usage of the Partition Keys, Sort keys, Global Secondary Index and the Local Secondary Index. This article builds on the previous article DynamoDB Introduction

Objective: Create 2 DynamoDB tables and illustrate the use of a Global Secondary Index and a Local Secondary Index.

DynamoDB Architecture Diagram

Prerequisites: It is assumed that you have all that is required to develop your AWS infrastructure with C# and your AWS Account. If you are unsure, verify that you have everything that was required for the previous tutorial Creating an AWS Serverless C# DotNet Application

  1. Create your DynamoDB CDK Project

Go to your projects folder, I am using C:\projects on my machine

Open a new command prompt and do the following

cd c:\projects
mkdir DynamoDBApp
cd DynamoDBApp
cdk init --language csharp

You should then be able to open the solution file located in C:\projects\DynamoDBApp\src\DynamoDbApp.sln with Visual Studio

The 2 files to examine are:

Program.cs: The entry point for your DynamoDBApp application

DynamoDBCdkAppStack.cs: This class will define your DynamoDBApp stack to determine which AWS Infrastructure resources get generated. It is currently empty.

Go to the Solution Explorer and right click to “Build Solution” you should get a successful build.

Go to the command line and execute the following commands to verify that the cdk project is working fine.

cd c:\projects\DynamoDBApp
cdk bootstrap
cdk synth
cdk deploy

If this runs without error, then it looks like you have a good setup to continue with.

3. Create the Lambda project

A Lambda project will allow us to interact with the DynamoDB tables interactively. Go to your current solution folder and create a Lambda template project.

cd c:\projects\DynamoDBApp
dotnet new -i Amazon.Lambda.Templates
dotnet new lambda.EmptyFunction --name Lambdas

ou should now see a Lambdas project that has been created for your Lambdas.

You can now add this to your DynamoDBApp Solution in Visual Studio.

Open Visual Studio, go to the Solution Explorer, right click , select Add, Existing Project…

Navigate to the newly created Lambdas project file

In my case it is located at C:\projects\DynamoDBApp\Lambdas\src\Lambdas\Lambdas.csproj

We can now rebuild the solution and we will have both the DynamoDBApp project and the Lambdas project successfully building.

cd c:\projects\DynamoDBApp
cdk bootstrap
cdk synth
cdk deploy

4. Update the DynamoDBAppStack.cs

To save time, we will copy the following code into our DynamoDbAppStack.cs file in the DynamoDbApp project. The actual code for DynamoDbAppStack.cs can be retrieved from https://github.com/xerris/DynamoDBApp or found in the Appendix of this article. This will determine the infrastructure for our DynamoDbApp project including the generation of an 2 DynamoDB Tables, and API Gateway API and 3 Lambdas.

Do not worry about any missing references for now, we will deal with them once we update the next file.

5. Update your Function.cs class

The next class is our Lambda Handler and will contain code for our 3 lambdas to interact with our 2 DynamoDbTables. The actual code for Function.cs can be retrieved from https://github.com/xerris/DynamoDBApp or found in the Appendix of this article.

6. Build your Solution Successfully

Go to the Solution Explorer and right click to “Build Solution” you will probably notice that there will be some Errors when you build your solution.

In this step we will ensure that the right packages are referenced for a successful build. The Errors will provide hints to which packages need to be installed in the solution.

Right click in the Solution Explorer and select “Manage Nuget Packages for Solution…”.

Required packages include

Amazon.CDK.AWS.DynamoDB

Amazon.Lambda.APIGatewayEvents

AWSSDK.DynamoDBv2

Amazon.CDK.AWS.APIGateway

Right click in Solution Explorer, Rebuild Solution

Now when you rebuild the Solution, it should build successfully. If not, the error should indicate the package you should install. (Or possibly if you need to add specific “using” statements at the top of your Function.cs or DynamoDbAppStack.cs classes.

7. Using Cloud Development Kit (CDK) to deploy your DynamoDb App to your AWS Cloud Environment

Open an command shell (CMD)

cd c:\projects\DynamoDBApp
cdk bootstrap
cdk synth
cdk deploy

You might be prompted to deploy these changes, just type Y and hit return

A successful deploy will look like the following:

Note the the outputs in blue will tell you key information about what you have deployed. There will be an 2 DynamoDB tables created. Along with this are 3 C# Lambdas that can be tested.

If you then look in your AWS Console you can see that there are 2 DynamoDB tables created for you:

2 DynamoDB Tables created

There will also be 3 Lambdas created as well:

3 Lambdas created

8. A look at the DynamoDB Table creation process

(a)Prd-User is basically a simple DynamoDB table with one Partition Key and one Primary Key of username. We should not have any records in it to start however. We are also going to create a Global Secondary index on this table on the email field.

Prd-User

The CDK Code to create this table can be seen in our DynamoDbAppStack.cs file as follows:

    //Create a User DynamoDB Table with a GSI (Primary Key Table)
            var userDynamoDbTable = new Table(this, "User", new TableProps
            {
                TableName = environment + "-" + "User",
                PartitionKey = new Amazon.CDK.AWS.DynamoDB.Attribute
                {
                    Name = "username",
                    Type = AttributeType.STRING
                },
                ReadCapacity = 1,
                WriteCapacity = 1,
                RemovalPolicy = RemovalPolicy.DESTROY
            });
//Adding the Global Secondary Index (GSI) 
            userDynamoDbTable.AddGlobalSecondaryIndex(new GlobalSecondaryIndexProps
            {
                IndexName = "UserGSIEmailIndex",
                PartitionKey = new Amazon.CDK.AWS.DynamoDB.Attribute
                {
                    Name = "email",
                    Type = AttributeType.STRING
                },
                ReadCapacity = 1,
                WriteCapacity = 1
            });

(b)Prd-City is basically a DynamoDB table with a Partition Key(state) and a Sort Key(city). It has a composite primary key of state and city together. We should not have any records in it to start however.

Prd-City

The CDK Code to create this table can be seen in our DynamoDbAppStack.cs file as follows:

//Create a City DynamoDB Table with an LSI (Composite Key Table)
            var cityDynamoDbTable = new Table(this, "City", new TableProps
            {
                TableName = environment + "-" + "City",
                PartitionKey = new Amazon.CDK.AWS.DynamoDB.Attribute
                {
                    Name = "state",
                    Type = AttributeType.STRING
                },
                SortKey = new Amazon.CDK.AWS.DynamoDB.Attribute
                {
                    Name = "city",
                    Type = AttributeType.STRING
                },
                ReadCapacity = 1,
                WriteCapacity = 1,
                RemovalPolicy = RemovalPolicy.DESTROY
            });
//Adding the Local Secondary Index (LSI)
            cityDynamoDbTable.AddLocalSecondaryIndex(new LocalSecondaryIndexProps
            {
                IndexName = "CityLSIPopulationIndex",
                SortKey = new Amazon.CDK.AWS.DynamoDB.Attribute
                {
                    Name = "population",
                    Type = AttributeType.NUMBER
                }
            });

9. Verifying the successful deployment

To test out your newly created DynamoDB Application(3 Lambdas), it is suggested that you get a tool like Postman to make calls to your Lambdas to test out the functionality.

(a) InitializeTables Lambda

This lambda will take a simple POST request and it will return a response.

You can confirm that there are no records in the DynamoDb table to start with in the AWS Console:

Open Postman up, Create a new Request. This request should be a POST.

Copy the url in your console window for your InitializeTablesLambda into your Postman request. Select the Body tab and select the raw option.

You will also note that in the Function.cs class in visual studio, there are sample JSON bodies provided to test each of the 3 lambdas. For this, lambda we can use the following json body(essentially empty):

{}

It should then look something like this:

You should then be able to press Send.

It should then send you back a response like the following:

We can then actually verify that this Lambda inserted 4 Users into the Primary Key table with a GSI and 6 Cities in the Composite Key table with an LSI.

The code that actually does all of the logic for this can be seen in the InitializeTablesLambdaHandler function in the Function.cs file. This is actually done in an object oriented fashion by utilizing a User class and City class.

All User DynamoDb functions are contained in the DynamoDbUserService class to try and keep it tidy.

There is also a corresponding DynamoDbCityService class in the Function.cs file to encapsulate the DynamoDB calls for the City table as well.

I invite the reader to examine the code and see how the calls work but saving a User object to it’s corresponding table looks like the following:

public void InsertUser(User user)
{
            dynamoDbContext.SaveAsync(user);
}

The user object is mapped to the DynamoDb table with the actual class definition as follows:

[DynamoDBTable("User")]
    public class User
    {
        [DynamoDBHashKey]
        public string username { get; set; }
        [DynamoDBProperty("firstname")]
        public string firstname { get; set; }
        [DynamoDBProperty("lastname")]
        public string lastname { get; set; }
        [DynamoDBProperty("email")]
        public string email { get; set; }
public override string ToString()
        {
            return "User: " + username + "," + firstname + "," + lastname + "," + email;
        }
    }

City is handled in a very similar fashion. If you run through the code in the InitializeTablesLambdaHandler function in Function.cs you can review how the code successfully saved data in both of the DynamoDb tables.

(b) TestPrimaryKeyDynamoDbLambda

This Lambda is designed to do a number of operations on the User DynamoDB Table which also has also has Global Secondary Index on the email table.

We will now run the command by copying the TestPrimaryKeyDynamoDBLambda url from the console to make another Postman request like you did in the previous step. You can set up the request body as follows:

{}

The resulting response will look something like:

The actual response should be something like:

{ "request":{},
"response":"TestPrimaryKeyTableLambdaHandler CDK Lambda Saturday, 13 March 2021 06:30:26 User DynamoDB Table:PRD-UserCity DynamoDB Table:PRD-City *(1) GetUsersByScan(All) collection size =4  User(0):User: JasonP,Jason,Pratt,jasonp@outlook.com    User(1):User: ThomasR,Thomas,Richards,tomr@abc.com    User(2):User: SteveJ,Steve,Jamieson,steve@gmail.com    User(3):User: JennyR,Jenny,Ross,jenny@hotmail.com   *(2)  GetUsersByScan(UserNames that startWith J) collection size =2  User(0):User: JasonP,Jason,Pratt,jasonp@outlook.com    User(1):User: JennyR,Jenny,Ross,jenny@hotmail.com   *(3) Delete username=JasonPDone delete operation *(4)After Delete collection size =1  User(0):User: JennyR,Jenny,Ross,jenny@hotmail.com   *(5)Querying user database With Partition Key :PRD-User***  GetUser(By Partition Key)=tomr@abc.com :0 *(6)Querying user database With GSI :PRD-UserGetUserByEmailGSI(By GSI) # Users in database with email=tomr@abc.com :1  User(0):User: ThomasR,Thomas,Richards,tomr@abc.com   *(7) Retrieving the current User Table(Primary Key) contents # Users=3",
"users":[{"username":"ThomasR","firstname":"Thomas","lastname":"Richards","email":"tomr@abc.com"},{"username":"SteveJ","firstname":"Steve","lastname":"Jamieson","email":"steve@gmail.com"},{"username":"JennyR","firstname":"Jenny","lastname":"Ross","email":"jenny@hotmail.com"}],
"cities":{},
"success":"True",
"message":""
}

Let us just review the 7 operations being done in this TestPrimaryKeyTableLambdaHandler Method

(i)Get All Users By Scan

List conditions = new List();
dbUserService.GetUsersByScan(environment, conditions).Wait();
public async Task GetUsersByScan(string environment, List conditions)
{
 var scanUsers = await this.dynamoDbContext.ScanAsync (conditions).GetRemainingAsync();
  this.users = new ArrayList();
  foreach (User u in scanUsers)
  {
    users.Add(u);
  }
}

(ii)Get Users by Scan (with a ScanOperator filter for those usernames that start with J

conditions = new List();
                conditions.Add(new ScanCondition("username", ScanOperator.BeginsWith, "J"));
                dbUserService.GetUsersByScan(environment, conditions).Wait();

(iii)Delete a User

string usernameToDelete = "JasonP";
dbUserService.DeleteUserByUserName(usernameToDelete).Wait();
public async Task DeleteUserByUserName(string username)
        {
            var Req = new Amazon.DynamoDBv2.Model.DeleteItemRequest
            {
                TableName = this.tableName,
                Key = new Dictionary() { { "username", new Amazon.DynamoDBv2.Model.AttributeValue { S = username.ToString() } } }
            };
            var response = await DynamoDbClientService.getDynamoDBClient(environment).DeleteItemAsync(Req);
            var attributeList = response.Attributes;
        }

(iv) Rerun the to see that this user was in fact deleted

Basically rerun the query done in Step 2 and confirm that there is only one user with a username that starts with J because username JasonP was deleted in Step 3

(v) Querying a user based on the partition key(username)

This should be more efficient that using a Scan as used above.

string searchEmail = "tomr@abc.com";
dbUserService.GetUser(searchEmail).Wait();
public async Task GetUser(string email)
{
            var user = await this.dynamoDbContext.LoadAsync(email);
            users = new ArrayList();
            if (user != null)
            {
                users.Add(user);
            }
}

(vi) Querying for a user based on the Global Secondary Index key(email)

The GSI allows us to search on another key rather than the default partition key.

string searchEmail = "tomr@abc.com";
dbUserService.GetUserByEmailGSI(searchEmail).Wait();

We use a QueryRequest object to query the table to find the user while using the GSI index as follows:

private string GSIEmailIndex = "UserGSIEmailIndex";
public async Task GetUserByEmailGSI(string email)
        {
            try
            {
                QueryRequest queryRequest = new QueryRequest
                {
                    TableName = this.tableName,
                    IndexName = this.GSIEmailIndex,
                    KeyConditionExpression = "#email = :email",
                    //placeholder that you use in an Amazon DynamoDB expression as an alternative to an actual attribute name
                    ExpressionAttributeNames = new Dictionary {
                        {"#email", "email"}
                    },
                    //substitutes for the actual values that you want to compare
                    ExpressionAttributeValues = new Dictionary {
                    {":email", new AttributeValue { S =  email }}
                    },
                    ScanIndexForward = true
                };
var response = await DynamoDbClientService.getDynamoDBClient(environment).QueryAsync(queryRequest);
                var items = response.Items;
this.users = new ArrayList();
                User currentUser = null;
                foreach (var currentItem in items)
                {
                    currentUser = new User();
                    foreach (string attr in currentItem.Keys)
                    {
if (attr == "username") { currentUser.username = currentItem[attr].S; }
if (attr == "firstname") { currentUser.firstname = currentItem[attr].S; };
if (attr == "lastname") { currentUser.lastname = currentItem[attr].S; };
if (attr == "email") { currentUser.email = currentItem[attr].S; };
                    }
                    this.users.Add(currentUser);
                }
}
            catch (Exception exc)
            {
                this.log += "EXC: " + exc.Message + ":" + exc.StackTrace;
            }
        }

(vii) Simply querying all of the users

This will just be a simple scan to confirm that there are now only 3 users in the user table as we deleted one of the initially created ones.

(c) TestCompositeKeyDbLambda

This Lambda is designed to do a number of operations on the City DynamoDB Table which also has also has Local Secondary Index on the email table. Note that this table has a composite key of state and city fields.

We will now run the command by copying the TestCompositeKeyDynamoDBLambda url from the console to make another Postman request like you did in the previous step. You can set up the request body as follows:

{}

The resulting response will look something like:

The response is a bit busy but this is a tutorial running through multiple DynamoDB operations in one Lambda. The actual response should be something like:

{ "request":{},
"response":"TestCompositeKeyTableLambdaHandler CDK Lambda Saturday, 13 March 2021 06:58:45 User DynamoDB Table:PRD-UserCity DynamoDB Table:PRD-CityCity Table Name :PRD-City *(1)Retrieving all Cities (by Scan) By Primary Key and default sort key(city)# Cities (by Scan)  By Primary Key and default sort key(city)=6  City(0):City: New York,Albany,Capital:True,Population:97478    City(1):City: New York,Buffalo,Capital:False,Population:261310    City(2):City: New York,New York,Capital:False,Population:8175133    City(3):City: California,Los Angeles,Capital:False,Population:3792621    City(4):City: California,Sacramento,Capital:True,Population:513624    City(5):City: California,San Diego,Capital:False,Population:1423851   *(2) Cities with population greater than 1 million (by Scan) =3  City(0):City: New York,New York,Capital:False,Population:8175133    City(1):City: California,Los Angeles,Capital:False,Population:3792621    City(2):City: California,San Diego,Capital:False,Population:1423851   *(3) Get all of cities in the state of:New York using the default sort key(Ascending)# Cities(By Primary Key) in the state of New York=3 using the default sort key(Ascending)City(0):City: New York,Albany,Capital:False,Population:97478  City(1):City: New York,Buffalo,Capital:False,Population:261310  City(2):City: New York,New York,Capital:False,Population:8175133   *(4) Get all of cities in the state of:New York using the default sort key(Descending)# Cities(By Primary Key) in the state of New York=3 using the default sort key(Descending)City(0):City: New York,New York,Capital:False,Population:8175133  City(1):City: New York,Buffalo,Capital:False,Population:261310  City(2):City: New York,Albany,Capital:False,Population:97478   *(5) Get all of cities in the state of:New York using the Population Local Secondary Index(Ascending)# Cities(By Primary Key) in the state of New York=3 using the Population Local Secondary Index (Ascending)City(0):City: New York,Albany,Capital:False,Population:97478  City(1):City: New York,Buffalo,Capital:False,Population:261310  City(2):City: New York,New York,Capital:False,Population:8175133   *(6) Get all of cities in the state of:New York using the Population Local Secondary Index(Descending)# Cities(By Primary Key) in the state of New York=3 using the Population Local Secondary Index(Descending)City(0):City: New York,New York,Capital:False,Population:8175133  City(1):City: New York,Buffalo,Capital:False,Population:261310  City(2):City: New York,Albany,Capital:False,Population:97478   *(7) Now trying to delete the city with the composite key of state=New York and city=Albany *(8) (Verify the Delete) Get all of cities in the state of:New York using the Population Local Secondary Index(Descending)# Cities(By Primary Key) in the state of New York=2 using the Population Local Secondary Index(Descending)City(0):City: New York,New York,Capital:False,Population:8175133  City(1):City: New York,Buffalo,Capital:False,Population:261310   *(9) Retrieving the City Table(Composite Key) contents # Cities=5",
"users":{},
"cities":[{"state":"New York","city":"Buffalo","iscapital":false,"population":261310},{"state":"New York","city":"New York","iscapital":false,"population":8175133},{"state":"California","city":"Los Angeles","iscapital":false,"population":3792621},{"state":"California","city":"Sacramento","iscapital":true,"population":513624},{"state":"California","city":"San Diego","iscapital":false,"population":1423851}],
"success":"True",
"message":""
}

Let us just review the 9 operations being done on the City(Composite Key table with an LSI) in this TestCompositeKeyTableLambdaHandler Method

(i)Retrieving all Cities(By Scan)

Here we are retrieving all 6 cities using a scan

List< ScanCondition > cityConditions = new List< ScanCondition >();
dbCityService.GetCities(conditions).Wait();

We basically pass the conditions into the ScanAsync method using the our DynamoDB context:

public async Task GetCities(List< ScanCondition > conditions)
        {
            var scanCities = await dynamoDbContext.ScanAsync(conditions).GetRemainingAsync();
            this.cities = new ArrayList();
            foreach (City city in scanCities)
            {
                this.cities.Add(city);
            }
        }

(ii) Retrieving all Cities with population greater than 1 million(By Scan)

We do the same as in the first scan except we will pass in a ScanOperator to indicate that on condition will be that the population be greater than or equal to one million. Note that this is different than a SQL based query

conditions = new List< ScanCondition >();
conditions.Add(new ScanCondition("population", ScanOperator.GreaterThanOrEqual, 1000000));
dbCityService.GetCities(conditions).Wait();
public async Task GetCities(List< ScanCondition > conditions)
        {
            var scanCities = await dynamoDbContext.ScanAsync(conditions).GetRemainingAsync();
            this.cities = new ArrayList();
            foreach (City city in scanCities)
            {
                this.cities.Add(city);
            }
        }

(iii)Get all Cities in state using default sort key(ascending)

dbCityService.GetCitiesByStateUsingDefaultSortKey(state, true).Wait();

The actual code below can be explored to show how a new QueryRequest object is created to retrieve all of the records that correspond to that state(partition key) and also retrieve them based on ascending order based on the sort key(city).

KeyConditionExpression: allows you to filter based on the specific value for the partition key

ExpressionAttributeNames: allows you to present substitution tokens in the expressions such as the KeyConditionExpression

ExpressionAttributeValues: allows you to define an expression attribute value as a placeholder

ProjectionExpression: the string that identifies the attributes you want returned

ScanIndexForward: whether to sort ascending or descending based on the sort key

You can also see the notation for parsing the returned items in the code section below. This is then used to hydrate objects to be returned to the business tier.

sample notation retrieving values from the DynamoDB query:

currentItem[“state”].S
currentItem[“iscapital”].BOOL
currentItem[“population].N

public async Task GetCitiesByStateUsingDefaultSortKey(string state, bool ascendingOrder)
        {
            try
            {
                //Note: For this Query Request
                //Sometimes you might encounter an exception such as:
                //Invalid KeyConditionExpression: Attribute name is a reserved keyword; reserved keyword: state:
                //It is for this reason that you will need to include the ExpressionAttributeNames below 
                //(for the KeyConditionExpression and the ProjectionExpression
//You can note that the records that are pulled for each state will be sorted by the default sort key(city)
                //ProjectExpression identifies the attributes that you want
var request = new QueryRequest
                {
                    TableName = this.tableName,
                    KeyConditionExpression = "#state = :state",
                    ExpressionAttributeNames = new Dictionary
                    {
                        { "#state", "state" },
                    },
                    ExpressionAttributeValues = new Dictionary {
                        {":state", new AttributeValue { S =  state }}
                    },
                    ProjectionExpression = "#state, city, iscapital, population",
                    ScanIndexForward = ascendingOrder,
                    ConsistentRead = true
                };
var response = await DynamoDbClientService.getDynamoDBClient(environment).QueryAsync(request);
                this.cities = new ArrayList();
                var items = response.Items;
City currentCity = null;
                foreach (var currentItem in items)
                {
                    currentCity = new City();
                    foreach (string attr in currentItem.Keys)
                    {
                        if (attr == "state") { currentCity.state = currentItem[attr].S; }
                        if (attr == "city") { currentCity.city = currentItem[attr].S; };
                        if (attr == "iscapital") { currentCity.iscapital = currentItem[attr].BOOL; };
                        if (attr == "population") { currentCity.population = int.Parse(currentItem[attr].N); };
                    }
                    this.cities.Add(currentCity);
}
}
            catch (Exception exc)
            {
                this.log += "Exception: " + exc.Message + ":" + exc.StackTrace;
            }
           
}

(iv) Get all Cities in a given state using the default key(descending)

To reverse the order we make the same call as in (iii) except that our second parameter will just reverse the ScanIndexForward value to change the sort order in the query

dbCityService.GetCitiesByStateUsingDefaultSortKey(state, false).Wait();

(v) Get all Cities in a state using the population local secondary index(LSI)(ascending)

Now we wish to query and sort by the population LSI and not the default sort key(city). The LSI helps us sort records.

dbCityService.GetCitiesByStateUsingPopulationLSI(state, true).Wait();

This is very similar to the the query used in (iii) and (iv) above except that we ensure we are using the LSI by including the following line:

IndexName = this.LSIPopulationIndex

private string LSIPopulationIndex = "CityLSIPopulationIndex";
public async Task GetCitiesByStateUsingPopulationLSI(string state, bool ascendingOrder)
        {
            try
            {
                //Note: For this Query Request
                //Sometimes you might encounter an exception such as:
                //Invalid KeyConditionExpression: Attribute name is a reserved keyword; reserved keyword: state:
                //It is for this reason that you will need to include the ExpressionAttributeNames below 
                //(for the KeyConditionExpression and the ProjectionExpression
//You can note that the records that are pulled for each state will be sorted by the default sort key(city)
//ProjectExpression identifies the attributes that you want
//ScanIndexForward will determine if you will use Ascending or descending order on the Sort Key(the LSI in this case)
                var request = new QueryRequest
                {
                    TableName = this.tableName,
                    IndexName = this.LSIPopulationIndex,
                    KeyConditionExpression = "#state = :state",
                    ExpressionAttributeNames = new Dictionary
                    {
                        { "#state", "state" },
                    },
                    ExpressionAttributeValues = new Dictionary {
                        {":state", new AttributeValue { S =  state }}
                    },
                    ProjectionExpression = "#state, city, iscapital, population",
                    ScanIndexForward = ascendingOrder,
                    ConsistentRead = true
                };
var response = await DynamoDbClientService.getDynamoDBClient(environment).QueryAsync(request);
                this.cities = new ArrayList();
                var items = response.Items;
City currentCity = null;
                foreach (var currentItem in items)
                {
                    currentCity = new City();
                    foreach (string attr in currentItem.Keys)
                    {
                        if (attr == "state") { currentCity.state = currentItem[attr].S; }
                        if (attr == "city") { currentCity.city = currentItem[attr].S; };
                        if (attr == "iscapital") { currentCity.iscapital = currentItem[attr].BOOL; };
                        if (attr == "population") { currentCity.population = int.Parse(currentItem[attr].N); };
                    }
                    this.cities.Add(currentCity);
                }
            }
            catch (Exception exc)
            {
                this.log += "Exception: " + exc.Message + ":" + exc.StackTrace;
            }
        }

(vi) Get all cities in a state using the population local secondary index(LSI)(descending)

We will make the same call as in (v) except we will pass in a false value which will alter the sort order to ascending for the ScanIndexForward value

dbCityService.GetCitiesByStateUsingPopulationLSI(state, false).Wait();
ScanIndexForward = ascendingOrder,

(vii)Deleting an entry in the City table

dbCityService.DeleteCitybyCompositeKey(stateToDelete, cityToDelete).Wait();

We will create a DeleteItemRequest and configure to find the record based on the composite key of state and city to delete the record

public async Task DeleteCitybyCompositeKey(string state, string city)
        {
            var deleteItemRequest = new Amazon.DynamoDBv2.Model.DeleteItemRequest
            {
                TableName = this.tableName,
                Key = new Dictionary()
                {
                    { "state", new Amazon.DynamoDBv2.Model.AttributeValue { S = state.ToString() } },
                    { "city", new Amazon.DynamoDBv2.Model.AttributeValue { S = city.ToString() } }
                }
            };
            var response = await DynamoDbClientService.getDynamoDBClient(environment).DeleteItemAsync(deleteItemRequest);
            var attributeList = response.Attributes;
}

(viii) Verify that the record is deleted by querying that same state

Simply run one of the queries of the table to ensure the record has been removed successfully

dbCityService.GetCitiesByStateUsingPopulationLSI(state, false).Wait();

(ix) Retrieving the City table contents based on an empty ScanCondition

Lastly, we query all the records to return all the existing cities in the json request and we obviously are missing the record that we deleted in the previous steps

conditions = new List();
                dbCityService.GetCities(conditions).Wait();
                citiesObjectJson = JsonSerializer.Serialize(dbCityService.cities);

10. Conclusion

This concludes this tutorial on a working example of DynamoDB tables with a Global Secondary Indexes and a Local Secondary Indexes. We have also presented running code for you to examine and do further investigation with.

Feel free to reach out if you have any questions regarding this article.

Appendix (2 Files)

Both files available at https://github.com/xerris/DynamoDBApp

(A) DynamoDBAppStack.cs

(B)Function.cs