.NET, AWS, C#, CSharp, Programming

AWS DynamoDB – 3 ways to query and read results in .NET

In my very first post, I showed how to use DynamoDB in .NET to receive single result (post). Looking into the evolution of API and layers of abstraction, we have much more options to query AWS DynamoDB. Therefore we have 3 ways to query and read results which I will demonstrate in this post.

The main difference is hidden in the way how mapping is done. Within mentioned options, we can distinct:

  1. Implicit mapping
  2. Mapping with ToDocument()methods
  3. Manual mapping

The best way, is to create a demo app and go thru examples. I created .NET 5 console application. Depending if you use “real” DynamoDB or mocked, you will have a bit different setup – mocked versions require to set ServiceURL instead of region on configuration object. In my examples, I will use Localstack – it’s not the best product, however it gives the best options from known products.

Let’s start with table creation:

aws dynamodb --endpoint-url=http://localhost:4566 create-table --table-name TestTable --key-schema AttributeName=HashKey,KeyType=HASH AttributeName=RangeKey,KeyType=Range --attribute-definitions AttributeName=HashKey,AttributeType=S AttributeName=RangeKey,AttributeType=S --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

Let’s feed the table with sample data:

aws dynamodb --endpoint-url=http://localhost:4566 put-item --table-name TestTable --item file://data.json

Where data.json content is:

{
    "HashKey" : {"S":"Key"},
    "RangeKey" : {"S":"0"}
}

Then our test model is:

public class Test
{
     public string HashKey { get; set; }
     public string RangeKey { get; set; }
}

As it’s nice to have more than just 1 record, you can modify entry and run command again. To validate if records are in the database, run command:

aws dynamodb --endpoint-url=http://localhost:4566 scan --table-name TestTable

Just be aware that scan is expensive operation and as long as you can, you should avoid it. As I’m using localstack and I’m not paying for the infrastructure, and I have just few records, it’s fine but it is not good idea to use it as above for production workloads.

So it’s time to code – add valid nuget package – AWSSDK.DynamoDBv2 – to your solution. The shared code in the considered solutions:

var config = new AmazonDynamoDBConfig
{
    ServiceURL = "http://localhost:4566"
};
var dynamoDbClient = new AmazonDynamoDBClient(config);
var dynamoDbContext = new DynamoDBContext(dynamoDbClient);

In this article, I consider only Query, however get single item is very similar.

Query with Table, Query method and FromDocuments mapping

Using Table class we need to LoadTable method to use document model for querying. In our scenario we have hash key and range key, therefore both are set in KeyExpression property. ExpressionStatment describes conditions. Hash key has to be always specified and also

var table = Table.LoadTable(dynamoDbClient, "TestTable");
var query = table.Query(new QueryOperationConfig
{
    KeyExpression = new Expression
    {
        ExpressionStatement = "HashKey = :hk AND RangeKey >= :rng",
        ExpressionAttributeValues =
        {
            {
               ":hk", "Key"
            },
            {
                ":rng", "0"
            }
        }
    }
});

var results = dynamoDbContext.FromDocuments<Test>(await query.GetNextSetAsync());

foreach (var res in results)
{
    Console.WriteLine($"{res.HashKey}:{res.RangeKey}");
}
Displayed results

Query with DynamoDBContext and model annotations

If you want to use context and model annotations you need small model’s modification:

[DynamoDBTable("TestTable")]
public class Test
{
    [DynamoDBHashKey]
    public string HashKey { get; set; }
    [DynamoDBRangeKey]
    public string RangeKey { get; set; }
}

The query is quite simple – it will return single result (as there is one item with hash key = “Key” and range key = “0”).

var results = await dynamoDbContext.QueryAsync<Test>("Key", QueryOperator.Equal, new List<object> { "0" }).GetNextSetAsync();

foreach (var res in results)
{
    Console.WriteLine($"{res.HashKey}:{res.RangeKey}");
}

You don’t need to specify table name in model. You can do it using additional parameter in QueryAsync. In such scenario the last one is of type DynamoDBOperationConfig and there you can set to override table name.

var results = await dynamoDbContext.QueryAsync<Test>("Key", QueryOperator.Equal, new List<object> { "0" }, new DynamoDBOperationConfig { OverrideTableName = "TestTable" }).GetNextSetAsync();
Displayed result

Query with DynamoDBClient, QueryRequest object and QueryAsync

In this approach we use QueryRequest to create data request. It’s the hardest approach however it gives you the most control over the query. Already in the query you need to know and specify DynamoDB data type of your keys. In the read part, you may note that I used only keys and S property (S = String, DynamoDB convention).

var queryResults = await dynamoDbClient.QueryAsync(new QueryRequest
{
    KeyConditionExpression = "HashKey = :hk AND RangeKey >= :rng",
    ExpressionAttributeValues = new Dictionary<string, AttributeValue>
    {
        { ":hk", new AttributeValue { S = "Key" } },
        { ":rng", new AttributeValue { S = "0" } },
    },
    Limit = 10,
    TableName = "TestTable"
});


foreach(var item in queryResults.Items)
{
    foreach(var key in item.Keys)
    {
        Console.Write($"{key}:{item[key].S} ");
    }
    Console.WriteLine();
}
Displayed result

Summary

Presented examples does not fulfill all possible variants of queries and response mapping.

There is also option for manual mapping, however I don’t recommend you to go this way. It’s not very streighforward and requires knowledge about underlying data structure (simplification: DynamoDB JSON is wrapped on your object with data types).

I encourage you to explore available options on your own and pick the most convinent to your scenario.

Leave a Reply

Your email address will not be published. Required fields are marked *