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 permissions –
DescribeTable
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 eachDescribeTable
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: