AWS Developer Tools Blog

Improved DynamoDB Initialization Patterns for the AWS SDK for .NET

The AWS SDK for .NET includes the Document and Object Persistence programming models, which provide an idiomatic .NET experience for working with Amazon DynamoDB. Beginning in AWSSDK.DynamoDBv2 3.7.203, there are new ways to initialize the document and object persistence models which can improve your application’s performance by reducing thread contention and throttling issues during the first call to DynamoDB.

In these high level models, the SDK relies on an internal cache of the DynamoDB table’s key and index structure to understand how to construct the low-level requests to DynamoDB’s API operations. The SDK automatically retrieves the table metadata by calling DynamoDB’s DescribeTable operation prior to calling any other DynamoDB operations. While convenient, there are some drawbacks surrounding the SDK’s reliance on the implicit DescribeTable call to fill its metadata cache:

  • Expanded permissionsDescribeTable requires an additional IAM action permission beyond what your application would need if it was just reading, writing, and querying data.
  • Throttling DescribeTable is a control plane operation, which is subject to a different requests-per-second limit than data plane operations
  • Cold-start latency – The initial DescribeTable call may appear as additional latency the first time your application is reading, writing, or querying data.
  • Thread pool starvation – The two high-level programming models predate the introduction of async/await in .NET, and offer both synchronous and asynchronous APIs. In newer versions of .NET that only offer an asynchronous HTTP client, the synchronous DynamoDB high-level APIs rely on the “sync over async” anti-pattern. This consumes multiple ThreadPool threads for each DescribeTable call that is filling the cache, which can lead to additional latency or deadlocks.

This post details three new ways to provide your table’s key and index structure via code. This will reduce cold-start latency and can avoid throttling and thread pool starvation issues, all by removing the SDK’s reliance on the implicit DescribeTable call.

Document Model and TableBuilder

Consider a table that stores replies to a threaded conversation. Today, you load the table definition explicitly by calling LoadTable or TryLoadTable, then interact with documents that represent items in that table:

var client = new AmazonDynamoDBClient();
var table = Table.LoadTable(client, "Reply"); // the SDK calls DescribeTable here

var newReply = new Document();
newReply["Id"] = Guid.NewGuid().ToString();
newReply["ReplyDateTime"] = DateTime.UtcNow;
newReply["PostedBy"] = "Author1";
newReply["Message"] = "Thank you!";

await table.PutItemAsync(newReply);

Now, instead of constructing the Table object via LoadTable or TryLoadTable you can use the new TableBuilder class. This allows you to provide the key and index structure of the Reply table in advance, which replaces the call to DescribeTable.

var client = new AmazonDynamoDBClient();

var table = new TableBuilder(client, "Reply")
    .AddHashKey("Id", DynamoDBEntryType.String)
    .AddRangeKey("ReplyDateTime", DynamoDBEntryType.String)
    .AddGlobalSecondaryIndex("PostedBy-Message-index", "Author", DynamoDBEntryType.String, "Message", DynamoDBEntryType.String)
    .Build();

var newReply = new Document();
newReply["Id"] = Guid.NewGuid().ToString();
newReply["ReplyDateTime"] = DateTime.UtcNow;
newReply["PostedBy"] = "Author1";
newReply["Message"] = "Thank you!";

await table.PutItemAsync(newReply);

Object Persistence Model and DisableFetchingTableMetadata

The object persistence model maps .NET classes to DynamoDB tables. The mapping is inferred from the classes’ public properties combined with the metadata retrieved from the DescribeTable call. The mapping can be customized by applying attributes to the .NET classes. Here is an example of a .NET class using the attributes to map the keys and indexes of a DynamoDB table.

[DynamoDBTable("Reply")]
public class Reply
{
    [DynamoDBHashKey]
    public string Id { get; set; }

    [DynamoDBRangeKey(StoreAsEpoch = false)]
    public DateTime ReplyDateTime { get; set; }

    [DynamoDBGlobalSecondaryIndexHashKey("PostedBy-Message-Index", AttributeName ="PostedBy")]
    public string Author { get; set; }

    [DynamoDBGlobalSecondaryIndexRangeKey("PostedBy-Message-Index")]
    public string Message { get; set; }
}

To be able to query for replies by a specific author, the SDK relies on knowing the PostedBy-Message-Index structure so that it can construct the KeyConditions property in the low-level Query request correctly:

var client = new AmazonDynamoDBClient();
var context = new DynamoDBContext(client);

// Constructs a query that will find all replies by a specific author,
// which relies on the global secondary index defined above
var query = context.QueryAsync<Reply>("Author1", new DynamoDBOperationConfig() { IndexName = "PostedBy-Message-index"});

Previously, even if you fully provided the key and index structure via the attributes, the SDK still called DescribeTable prior to the first Query operation. This validated the information provided in the attributes, as well as filled in missing key or index information in some cases. The table metadata is then cached for subsequent Query operations.

Now, to avoid this DescribeTable call and rely entirely on the attributes, you can set DisableFetchingTableMetadata to true on DynamoDBContextConfig. You must describe the key and indexes accurately via the attributes on the .NET classes so that the SDK can continue building the underlying requests correctly.

var client = new AmazonDynamoDBClient();
var config = new DynamoDBContextConfig
{
    DisableFetchingTableMetadata = true
};

var context = new DynamoDBContext(client, config);

// Constructs an identical query to the example above
var query = context.QueryAsync<Reply>("Author1", new DynamoDBOperationConfig() { IndexName = "PostedBy-Message-index"});

Alternatively you can set this property globally via AWSConfigsDynamoDB.Context. You can also set it from your app.config or web.config file if using .NET Framework.

// Set this globally before constructing any context objects
AWSConfigsDynamoDB.Context.DisableFetchingTableMetadata = true;

var client = new AmazonDynamoDBClient();
var context = new DynamoDBContext(client);

Object Persistence Model and TableBuilder

You can also combine the above techniques, and register the Table object created by a TableBuilder object with your DynamoDBContext instance. This may allow you to use the object persistence model even if you are unable to add the DynamoDB attributes to the .NET class, such as if the class is defined in a dependency.

var client = new AmazonDynamoDBClient();
var config = new DynamoDBContextConfig
{
    DisableFetchingTableMetadata = true
};

var context = new DynamoDBContext(client, config);

var table = new TableBuilder(client, "Reply")
    .AddHashKey("Id", DynamoDBEntryType.String)
    .AddRangeKey("ReplyDateTime", DynamoDBEntryType.String)
    .AddGlobalSecondaryIndex("PostedBy-Message-index", "Author", DynamoDBEntryType.String, "Message", DynamoDBEntryType.String)
    .Build();

// This registers the "Reply" table we constructed via the builder
context.RegisterTableDefinition(table);

// So operations like this will work,
// even if the Reply class was not annotated with this index
var query = context.QueryAsync<Reply>("Author1", new DynamoDBOperationConfig() { IndexName = "PostedBy-Message-index"});

Tradeoffs

Whether using DisableFetchingTableMetadata or TableBuilder, it is important to describe your table’s key and indexes accurately in your code. Otherwise you may see exceptions thrown when attempting to perform DynamoDB read, write, and query operations.

The Table object created via the new initialization methods will not contain some information that is populated the DescribeTable call, though this does not impact the SDK’s ability to construct the low-level requests. Specifically properties inside its LocalSecondaryIndexDescription and  GlobalSecondaryIndexDescription objects will be null.

Conclusion

If you use the AWS SDK for .NET’s document or object persistence programming models for DynamoDB, we recommend trying these initialization APIs, especially if you are seeing issues with the automatic DescribeTable call. They can reduce thread contention and throttling issues, which can improve your application’s performance. Download AWSSDK.DynamoDBv2 version 3.7.203 or later from NuGet to try them out. Don’t hesitate to create an issue or a pull request if you have ideas for improvements.

About the author:

Alex Shovlin

Alex Shovlin

Alex Shovlin is a software development engineer on the .NET SDK team at AWS. He enjoys working on projects and tools that aim to improve the developer experience. You can find him on GitHub @ashovlin.