diff --git a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
index 4101648ecd2d..f87807db6755 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
@@ -53,6 +53,61 @@ public partial class DynamoDBContext : IDynamoDBContext
#endregion
+ #region Public methods
+ ///
+ /// Adds a to this context's internal cache, which
+ /// will avoid the need to fetch table metadata automatically from DynamoDB.
+ /// This may be used in conjunction with an .
+ ///
+ ///
+ /// Using this method can avoid latency and potential deadlocks due to the internal
+ /// call that is used to populate
+ /// the SDK's cache of table metadata. It requires that the table's index schema described accurately,
+ /// otherwise exceptions may be thrown and/or the results of certain DynamoDB operations may change.
+ /// It is recommended to test your application prior to using this in production code.
+ ///
+ /// Table to add to the internal cache
+ public void RegisterTableDefinition(Table table)
+ {
+ try
+ {
+ _readerWriterLockSlim.EnterReadLock();
+
+ if (tablesMap.ContainsKey(table.TableName))
+ {
+ return;
+ }
+ }
+ finally
+ {
+ if (_readerWriterLockSlim.IsReadLockHeld)
+ {
+ _readerWriterLockSlim.ExitReadLock();
+ }
+ }
+
+ try
+ {
+ _readerWriterLockSlim.EnterWriteLock();
+
+ // Check to see if another thread go the write lock before this thread and filled the cache.
+ if (tablesMap.ContainsKey(table.TableName))
+ {
+ return;
+ }
+
+ tablesMap[table.TableName] = table;
+ }
+ finally
+ {
+ if (_readerWriterLockSlim.IsWriteLockHeld)
+ {
+ _readerWriterLockSlim.ExitWriteLock();
+ }
+ }
+ }
+ #endregion
+
#region Constructors
#if !NETSTANDARD
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/ITableBuilder.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/ITableBuilder.cs
new file mode 100644
index 000000000000..8b1cb615930e
--- /dev/null
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/ITableBuilder.cs
@@ -0,0 +1,61 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+namespace Amazon.DynamoDBv2.DocumentModel
+{
+ ///
+ /// Interface for a builder that constructs a
+ ///
+ public interface ITableBuilder
+ {
+ ///
+ /// Call at the end to retrieve the new
+ ///
+ /// Built table
+ Table Build();
+
+ ///
+ /// Adds the primary key definition to the table
+ ///
+ /// Name of the attribute used as the partition key
+ /// Type of that attribute
+ ITableBuilder AddHashKey(string hashKeyAttribute, DynamoDBEntryType type);
+
+ ///
+ /// Adds a sort key definition to the table
+ ///
+ /// Name of the attribute used as the sort key
+ /// Type of that attribute
+ ITableBuilder AddRangeKey(string rangeKeyAttribute, DynamoDBEntryType type);
+
+ ///
+ /// Adds a local secondary index definition to the table
+ ///
+ /// Name of the local secondary index
+ /// Name of the attribute used as the sort key in the local secondary index
+ /// Type of that attribute
+ ITableBuilder AddLocalSecondaryIndex(string indexName, string rangeKeyAttribute, DynamoDBEntryType type);
+
+ ///
+ /// Adds a global secondary index definition to the table
+ ///
+ /// Name of the global secondary index
+ /// Name of the attribute used as the partition key in the GSI
+ /// Type of the hash key attribute
+ /// Name of the attribute used as the sort key in the GSI
+ /// Type of the sort key attribute
+ ITableBuilder AddGlobalSecondaryIndex(string indexName, string hashkeyAttribute, DynamoDBEntryType hashKeyType, string rangeKeyAttribute, DynamoDBEntryType rangeKeyType);
+ }
+}
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs
index 72125f8bc4f2..af6da4768dfb 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs
@@ -340,7 +340,7 @@ private static void ValidateConditional(IConditionalOperationConfig config)
throw new InvalidOperationException("Only one of the conditonal properties Expected, ExpectedState and ConditionalExpression can be set.");
}
- private void ClearTableData()
+ internal void ClearTableData()
{
Keys = new Dictionary();
HashKeys = new List();
@@ -399,7 +399,7 @@ internal Dictionary ToAttributeMap(Document doc, DynamoD
#region Constructor/factory
- private Table(IAmazonDynamoDB ddbClient, TableConfig config)
+ internal Table(IAmazonDynamoDB ddbClient, TableConfig config)
{
if (config == null)
throw new ArgumentNullException("config");
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/TableBuilder.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/TableBuilder.cs
new file mode 100644
index 000000000000..a94dd76bca1a
--- /dev/null
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/TableBuilder.cs
@@ -0,0 +1,260 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+using Amazon.DynamoDBv2.Model;
+using System;
+using System.Collections.Generic;
+
+namespace Amazon.DynamoDBv2.DocumentModel
+{
+ ///
+ /// Builder that constructs a
+ ///
+ public class TableBuilder : ITableBuilder
+ {
+ ///
+ /// The object that is built and then returned from
+ ///
+ private Table _table;
+
+ ///
+ /// Keeps track internally of attributes that have already been saved in ,
+ /// since they can be shared by different indices.
+ ///
+ private HashSet _attributesAlreadyProcessed;
+
+ ///
+ /// Creates a builder object to construct a
+ ///
+ /// Client to use to access DynamoDB.
+ /// Table name
+ public TableBuilder(IAmazonDynamoDB ddbClient, string tableName) :
+ this(ddbClient, tableName, DynamoDBEntryConversion.CurrentConversion, false, null)
+ {
+ }
+
+ ///
+ /// Creates a builder object to construct a
+ ///
+ /// Client to use to access DynamoDB.
+ /// Table name
+ /// Conversion to use for converting .NET values to DynamoDB values.
+ /// If the property is false, empty string values will be interpreted as null values.
+ /// The document API relies on an internal cache of the DynamoDB table's metadata to construct and validate
+ /// requests. This controls how the cache key is derived, which influences when the SDK will call
+ /// internally to populate the cache.
+ public TableBuilder(IAmazonDynamoDB ddbClient, string tableName, DynamoDBEntryConversion conversion, bool isEmptyStringValueEnabled, MetadataCachingMode? metadataCachingMode)
+ : this (ddbClient, new TableConfig(tableName, conversion, Table.DynamoDBConsumer.DocumentModel, null, isEmptyStringValueEnabled, metadataCachingMode))
+ {
+ }
+
+ ///
+ /// Creates a builder object to construct a
+ ///
+ /// Client to use to access DynamoDB.
+ /// Configuration to use for the table.
+ public TableBuilder(IAmazonDynamoDB ddbClient, TableConfig config)
+ {
+ _table = new Table(ddbClient, config);
+ _table.ClearTableData(); // initializes internal collections
+ _attributesAlreadyProcessed = new HashSet();
+ }
+
+ ///
+ public Table Build()
+ {
+ if (_table.HashKeys.Count == 0)
+ {
+ throw new ArgumentOutOfRangeException("A partition key definition is required, call AddHashKey before Build.");
+ }
+
+ return _table;
+ }
+
+ ///
+ public ITableBuilder AddHashKey(string hashKeyAttribute, DynamoDBEntryType type)
+ {
+ if (string.IsNullOrEmpty(hashKeyAttribute))
+ {
+ throw new ArgumentNullException(nameof(hashKeyAttribute), "The name of the partition key attribute is required.");
+ }
+
+ if (_table.HashKeys.Count != 0)
+ {
+ throw new ArgumentOutOfRangeException("Only a single partition key is supported, and one has already been added.");
+ }
+
+ _table.HashKeys.Add(hashKeyAttribute);
+
+ _table.Keys.Add(hashKeyAttribute, new KeyDescription
+ {
+ IsHash = true,
+ Type = type
+ });
+
+ if (!_attributesAlreadyProcessed.Contains(hashKeyAttribute))
+ {
+ _table.Attributes.Add(new AttributeDefinition(hashKeyAttribute, dynamoDBEntryTypeToScalarAttributeType(type)));
+ _attributesAlreadyProcessed.Add(hashKeyAttribute);
+ }
+
+ return this;
+ }
+
+ ///
+ public ITableBuilder AddRangeKey(string rangeKeyAttribute, DynamoDBEntryType type)
+ {
+ if (string.IsNullOrEmpty(rangeKeyAttribute))
+ {
+ throw new ArgumentNullException(nameof(rangeKeyAttribute), "The name of the sort key attribute is required.");
+ }
+
+ if (_table.RangeKeys.Count != 0)
+ {
+ throw new ArgumentOutOfRangeException("Only a single sort key is supported, and one has already been added.");
+ }
+
+ _table.RangeKeys.Add(rangeKeyAttribute);
+
+ _table.Keys.Add(rangeKeyAttribute, new KeyDescription
+ {
+ IsHash = false,
+ Type = type
+ });
+
+ if (!_attributesAlreadyProcessed.Contains(rangeKeyAttribute))
+ {
+ _table.Attributes.Add(new AttributeDefinition(rangeKeyAttribute, dynamoDBEntryTypeToScalarAttributeType(type)));
+ _attributesAlreadyProcessed.Add(rangeKeyAttribute);
+ }
+
+ return this;
+ }
+
+ ///
+ public ITableBuilder AddLocalSecondaryIndex(string indexName, string rangeKeyAttribute, DynamoDBEntryType type)
+ {
+ if (string.IsNullOrEmpty(indexName))
+ {
+ throw new ArgumentNullException(nameof(indexName), "The name of the local secondary index is required");
+ }
+
+ if (_table.LocalSecondaryIndexes.ContainsKey(indexName))
+ {
+ throw new ArgumentException($"An local secondary index with name {indexName} has already been defined.");
+ }
+
+ if (string.IsNullOrEmpty(rangeKeyAttribute))
+ {
+ throw new ArgumentNullException(nameof(rangeKeyAttribute), "The attribute name of the range key within the local secondary index is required.");
+ }
+
+ if (_table.HashKeys.Count == 0)
+ {
+ throw new ArgumentException("A local secondary index uses the table's partition key, which was not provided. Call AddHashKey prior to AddLocalSecondaryIndex.");
+
+ }
+
+ _table.LocalSecondaryIndexNames.Add(indexName);
+
+ _table.LocalSecondaryIndexes.Add(indexName, new LocalSecondaryIndexDescription
+ {
+ IndexName = indexName,
+ KeySchema = new List()
+ {
+ new KeySchemaElement { AttributeName = _table.HashKeys[0], KeyType = KeyType.HASH },
+ new KeySchemaElement { AttributeName = rangeKeyAttribute, KeyType = KeyType.RANGE }
+ }
+ });
+
+ if (!_attributesAlreadyProcessed.Contains(rangeKeyAttribute))
+ {
+ _table.Attributes.Add(new AttributeDefinition(rangeKeyAttribute, dynamoDBEntryTypeToScalarAttributeType(type)));
+ _attributesAlreadyProcessed.Add(rangeKeyAttribute);
+ }
+
+ return this;
+ }
+
+ ///
+ public ITableBuilder AddGlobalSecondaryIndex(string indexName, string hashkeyAttribute, DynamoDBEntryType hashKeyType, string rangeKeyAttribute, DynamoDBEntryType rangeKeyType)
+ {
+ if (string.IsNullOrEmpty(indexName))
+ {
+ throw new ArgumentNullException(nameof(indexName), "The name of the global secondary index is required");
+ }
+
+ if (_table.GlobalSecondaryIndexes.ContainsKey(indexName))
+ {
+ throw new ArgumentException($"An global secondary index with name {indexName} has already been defined.");
+ }
+
+ if (string.IsNullOrEmpty(hashkeyAttribute))
+ {
+ throw new ArgumentNullException(nameof(hashkeyAttribute), "The attribute name of the partition key within the global secondary index is required.");
+ }
+
+ if (string.IsNullOrEmpty(rangeKeyAttribute))
+ {
+ throw new ArgumentNullException(nameof(rangeKeyAttribute), "The attribute name of the range key within the global secondary index is required.");
+ }
+
+ _table.GlobalSecondaryIndexNames.Add(indexName);
+
+ _table.GlobalSecondaryIndexes.Add(indexName, new GlobalSecondaryIndexDescription
+ {
+ IndexName = indexName,
+ KeySchema = new List()
+ {
+ new KeySchemaElement { AttributeName = hashkeyAttribute, KeyType = KeyType.HASH },
+ new KeySchemaElement { AttributeName = rangeKeyAttribute, KeyType = KeyType.RANGE }
+ }
+ });
+
+ if (!_attributesAlreadyProcessed.Contains(hashkeyAttribute))
+ {
+ _table.Attributes.Add(new AttributeDefinition(hashkeyAttribute, dynamoDBEntryTypeToScalarAttributeType(hashKeyType)));
+ _attributesAlreadyProcessed.Add(hashkeyAttribute);
+ }
+
+ if (!_attributesAlreadyProcessed.Contains(rangeKeyAttribute))
+ {
+ _table.Attributes.Add(new AttributeDefinition(rangeKeyAttribute, dynamoDBEntryTypeToScalarAttributeType(rangeKeyType)));
+ _attributesAlreadyProcessed.Add(rangeKeyAttribute);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Maps the document model's to the corresponding, low-level
+ ///
+ /// Document model attribute type
+ /// Corresponding low-level attribute type
+ private static ScalarAttributeType dynamoDBEntryTypeToScalarAttributeType(DynamoDBEntryType entryType)
+ {
+ switch (entryType)
+ {
+ case DynamoDBEntryType.String:
+ return ScalarAttributeType.S;
+ case DynamoDBEntryType.Numeric:
+ return ScalarAttributeType.N;
+ case DynamoDBEntryType.Binary:
+ return ScalarAttributeType.B;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(entryType), "Not a valid ScalarAttributeType");
+ }
+ }
+ }
+}
diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
index f4f2c0c17411..c0e792e82b24 100644
--- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
+++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
@@ -159,6 +159,66 @@ public void TestContext_DisableFetchingTableMetadata_DateTimeAsHashKey()
}
+ ///
+ /// Runs the same object-mapper integration tests as ,
+ /// but using table definitions created by instead of the internal call
+ ///
+ [TestMethod]
+ [TestCategory("DynamoDBv2")]
+ public void TestWithBuilderTables()
+ {
+ foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 })
+ {
+ // Cleanup existing data in the tables
+ CleanupTables();
+
+ // Clear existing SDK-wide cache
+ TableCache.Clear();
+
+ // Redeclare Context, which will start with empty caches
+ Context = new DynamoDBContext(Client, new DynamoDBContextConfig
+ {
+ IsEmptyStringValueEnabled = true,
+ Conversion = conversion
+ });
+
+ Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-HashRangeTable")
+ .AddHashKey("Name", DynamoDBEntryType.String)
+ .AddRangeKey("Age", DynamoDBEntryType.Numeric)
+ .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Score", DynamoDBEntryType.Numeric)
+ .AddLocalSecondaryIndex("LocalIndex", "Manager", DynamoDBEntryType.String)
+ .Build());
+
+ Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-HashTable")
+ .AddHashKey("Id", DynamoDBEntryType.Numeric)
+ .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Price", DynamoDBEntryType.Numeric)
+ .Build());
+
+ Context.RegisterTableDefinition(new TableBuilder(Client, "DotNetTests-NumericHashRangeTable")
+ .AddHashKey("CreationTime", DynamoDBEntryType.Numeric)
+ .AddRangeKey("Name", DynamoDBEntryType.String)
+ .Build());
+
+ TestEmptyStringsWithFeatureEnabled();
+
+ TestEnumHashKeyObjects();
+
+ TestEmptyCollections(conversion);
+
+ TestUnsupportedTypes();
+ TestEnums(conversion);
+
+ TestHashObjects();
+ TestHashRangeObjects();
+ TestOtherContextOperations();
+ TestBatchOperations();
+ TestTransactionOperations();
+ TestMultiTableTransactionOperations();
+
+ TestStoreAsEpoch();
+ }
+ }
+
private static void TestEmptyStringsWithFeatureEnabled()
{
var product = new Product
diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs
index ed0e2cd52ab9..a7173f61ca13 100644
--- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs
+++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs
@@ -81,6 +81,82 @@ public void TestTableOperations()
}
}
+ ///
+ /// Runs the same tests as , but with
+ /// static table definitions that avoid the internal call to populate the cache
+ ///
+ [TestMethod]
+ public void TestTableOperationsViaBuilder()
+ {
+ foreach (var conversion in new DynamoDBEntryConversion[] { DynamoDBEntryConversion.V1, DynamoDBEntryConversion.V2 })
+ {
+ // Clear tables
+ CleanupTables();
+
+ var hashTable = new TableBuilder(Client, "DotNetTests-HashTable", conversion, true, null)
+ .AddHashKey("Id", DynamoDBEntryType.Numeric)
+ .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Price", DynamoDBEntryType.Numeric)
+ .Build();
+
+
+ var hashRangeTable = new TableBuilder(Client, "DotNetTests-HashRangeTable", conversion, true, null)
+ .AddHashKey("Name", DynamoDBEntryType.String)
+ .AddRangeKey("Age", DynamoDBEntryType.Numeric)
+ .AddGlobalSecondaryIndex("GlobalIndex", "Company", DynamoDBEntryType.String, "Score", DynamoDBEntryType.Numeric)
+ .AddLocalSecondaryIndex("LocalIndex", "Manager", DynamoDBEntryType.String)
+ .Build();
+
+ var numericHashRangeTable = new TableBuilder(Client, "DotNetTests-NumericHashRangeTable", conversion, true, null)
+ .AddHashKey("CreationTime", DynamoDBEntryType.Numeric)
+ .AddRangeKey("Name", DynamoDBEntryType.String)
+ .Build();
+
+ TestEmptyString(hashTable);
+
+ // Test saving and loading empty lists and maps
+ TestEmptyCollections(hashTable);
+
+ // Test operations on hash-key table
+ TestHashTable(hashTable, conversion);
+
+ // Test operations on hash-and-range-key table
+ TestHashRangeTable(hashRangeTable, conversion);
+
+ // Test using multiple test batch writer
+ TestMultiTableDocumentBatchWrite(hashTable, hashRangeTable);
+
+ // Test multi-table transactional operations
+ TestMultiTableDocumentTransactWrite(hashTable, hashRangeTable, conversion);
+
+ // Test large batch writes and gets
+ TestLargeBatchOperations(hashTable);
+
+ // Test expressions for update
+ TestExpressionUpdate(hashTable);
+
+ // Test expressions for put
+ TestExpressionPut(hashTable);
+
+ // Test expressions for delete
+ TestExpressionsOnDelete(hashTable);
+
+ // Test expressions for transactional operations
+ TestExpressionsOnTransactWrite(hashTable, conversion);
+
+ // Test expressions for query
+ TestExpressionsOnQuery(hashRangeTable);
+
+ // Test expressions for scan
+ TestExpressionsOnScan(hashRangeTable);
+
+ // Test Query and Scan manual pagination
+ TestPagination(hashRangeTable);
+
+ // Test storing some attributes as epoch seconds
+ TestStoreAsEpoch(hashRangeTable, numericHashRangeTable);
+ }
+ }
+
private void TestEmptyString(Table hashTable)
{
var companyInfo = new DynamoDBList();
diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/TableBuilderTests.cs b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/TableBuilderTests.cs
new file mode 100644
index 000000000000..0ecb0a19b154
--- /dev/null
+++ b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/TableBuilderTests.cs
@@ -0,0 +1,189 @@
+using Amazon.DynamoDBv2;
+using Amazon.DynamoDBv2.DocumentModel;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+
+namespace AWSSDK_DotNet35.UnitTests
+{
+ ///
+ /// Tests for
+ ///
+ [TestClass]
+ public class TableBuilderTests
+ {
+ private IAmazonDynamoDB _amazonDynamoDBClient = new AmazonDynamoDBClient();
+
+ ///
+ /// Asserts that the table requires a partition key definition
+ ///
+ [TestMethod]
+ public void MissingPrimaryKey_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+
+ Assert.ThrowsException(() => builder.Build());
+ }
+
+ ///
+ /// Asserts that the partition key requires a non-null attribute
+ ///
+ [TestMethod]
+ public void NullPrimaryKey_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+
+ Assert.ThrowsException(() => builder.AddHashKey("", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when more that one partition key is defined
+ ///
+ [TestMethod]
+ public void MoreThanOnePrimaryKey_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddHashKey("Id", DynamoDBEntryType.String);
+
+ Assert.ThrowsException(() => builder.AddHashKey("Id2", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the partition key requires a non-null attribute
+ ///
+ [TestMethod]
+ public void NullRangeKey_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+
+ Assert.ThrowsException(() => builder.AddRangeKey("", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when more that one sort key is defined
+ ///
+ [TestMethod]
+ public void MoreThanOneRangeKey_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddRangeKey("Date", DynamoDBEntryType.String);
+
+ Assert.ThrowsException(() => builder.AddRangeKey("Date2", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when a LSI is defined before the partition key is,
+ /// since the LSI reuses the partition key
+ ///
+ [TestMethod]
+ public void LSIWithoutPrimaryKey_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+
+ Assert.ThrowsException(() => builder.AddLocalSecondaryIndex("LSI", "Date", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when a LSI is defined without an index name
+ ///
+ [TestMethod]
+ public void MissingLSIIndexName_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddHashKey("Id", DynamoDBEntryType.String);
+
+ Assert.ThrowsException(() => builder.AddLocalSecondaryIndex("", "Date", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when a LSI is defined without an attribute name
+ ///
+ [TestMethod]
+ public void MissingLSIAttribute_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddHashKey("Id", DynamoDBEntryType.String);
+
+ Assert.ThrowsException(() => builder.AddLocalSecondaryIndex("LSI", "", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when a duplicate LSI is defined
+ ///
+ [TestMethod]
+ public void DuplicateLSIName_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddHashKey("Id", DynamoDBEntryType.String);
+ builder.AddLocalSecondaryIndex("LSI", "Date", DynamoDBEntryType.String);
+
+ Assert.ThrowsException(() => builder.AddLocalSecondaryIndex("LSI", "Date2", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder only stores key attributes once when reused in an LSI
+ ///
+ [TestMethod]
+ public void AttributeReusedViaLSI_OnlyStoredOnce()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddHashKey("Id", DynamoDBEntryType.String);
+
+ // Add a GSI with two new attributes
+ builder.AddGlobalSecondaryIndex("GSI", "Date", DynamoDBEntryType.String, "OrderNumber", DynamoDBEntryType.Numeric);
+
+ // Add a LSI that reuses one of those, which shouldn't be stored in table.Attributes again
+ builder.AddLocalSecondaryIndex("LSI", "OrderNumber", DynamoDBEntryType.Numeric);
+
+ var table = builder.Build();
+
+ Assert.AreEqual(3, table.Attributes.Count);
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when any of the required GSI identifiers are null
+ ///
+ public void GSINullNames_ThrowException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+
+ // Null GSI index name
+ Assert.ThrowsException(() => builder.AddGlobalSecondaryIndex("", "Id", DynamoDBEntryType.String, "Date", DynamoDBEntryType.String));
+
+ // Null partition key
+ Assert.ThrowsException(() => builder.AddGlobalSecondaryIndex("GSI", "", DynamoDBEntryType.String, "Date", DynamoDBEntryType.String));
+
+ // Null sort key
+ Assert.ThrowsException(() => builder.AddGlobalSecondaryIndex("GSI", "Id", DynamoDBEntryType.String, "", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder throws an exception when a duplicate GSI is defined
+ ///
+ [TestMethod]
+ public void DuplicateGSIName_ThrowsException()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddGlobalSecondaryIndex("GSI", "Id", DynamoDBEntryType.String, "Date", DynamoDBEntryType.String);
+
+ Assert.ThrowsException(() => builder.AddGlobalSecondaryIndex("GSI", "Id", DynamoDBEntryType.String, "Date", DynamoDBEntryType.String));
+ }
+
+ ///
+ /// Asserts that the builder only stores key attributes once when they are reused in a GSI
+ ///
+ [TestMethod]
+ public void AttributesResuedViaGSI_OnlyStoredOnce()
+ {
+ var builder = new TableBuilder(_amazonDynamoDBClient, "TestTable");
+ builder.AddHashKey("Id", DynamoDBEntryType.String);
+ builder.AddRangeKey("Date", DynamoDBEntryType.String);
+
+ // Add a GSI that uses those same two attributes
+ builder.AddGlobalSecondaryIndex("GSI", "Date", DynamoDBEntryType.String, "Id", DynamoDBEntryType.String);
+
+ var table = builder.Build();
+
+ Assert.AreEqual(2, table.Attributes.Count);
+ }
+ }
+}