/* * 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 System; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; using Amazon.Util; using System.Linq; using System.Threading; #if AWS_ASYNC_API using System.Threading.Tasks; #endif using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Text; using Amazon.DynamoDBv2.DataModel; using Amazon.Runtime.Internal.Util; namespace Amazon.DynamoDBv2.DocumentModel { /// /// The Table class is the starting object when using the Document API. It is used to Get documents from the DynamnoDB table /// and write documents back to the DynamoDB table. /// public partial class Table { #region Private/internal members internal enum DynamoDBConsumer { DocumentModel, DataModel, SessionStateProvider } private static string TableInfoCacheIdentifier = typeof(Table).FullName; private TableConfig Config; internal Table.DynamoDBConsumer TableConsumer { get { return Config.Consumer; } } internal DynamoDBEntryConversion Conversion { get { return Config.Conversion; } } internal bool IsEmptyStringValueEnabled { get {return Config.IsEmptyStringValueEnabled; } } internal IEnumerable StoreAsEpoch { get { return Config.AttributesToStoreAsEpoch; } } internal IEnumerable KeyNames { get { return Keys.Keys; } } #if NETSTANDARD internal AmazonDynamoDBClient DDBClient { get; private set; } #else internal IAmazonDynamoDB DDBClient { get; private set; } #endif #endregion #region Public properties /// /// Name of the table. /// public string TableName { get { return Config.TableName; } } /// /// Keys of the table. /// public Dictionary Keys { get; private set; } /// /// Global secondary indexes of the table. /// public Dictionary GlobalSecondaryIndexes { get; private set; } /// /// Local secondary indexes of the table. /// public Dictionary LocalSecondaryIndexes { get; private set; } /// /// Names of the local secondary indexes of the table. /// public List LocalSecondaryIndexNames { get; private set; } /// /// Names of the global secondary indexes of the table. /// public List GlobalSecondaryIndexNames { get; private set; } /// /// List of keys on the table marked HASH /// public List HashKeys { get; private set; } /// /// List of keys on the table marked RANGE /// public List RangeKeys { get; private set; } /// /// List of key attributes on the table. /// public List Attributes { get; set; } #endregion #region Private/internal methods private static DynamoDBEntryType GetType(string attributeType) { if (String.Equals(attributeType, "N")) return DynamoDBEntryType.Numeric; if (String.Equals(attributeType, "S")) return DynamoDBEntryType.String; if (String.Equals(attributeType, "B")) return DynamoDBEntryType.Binary; throw new InvalidOperationException("Unknown attribute type"); } private void LoadTableInfo() { ClearTableData(); var tableInfoCache = SdkCache.GetCache(DDBClient, TableInfoCacheIdentifier, StringComparer.Ordinal); bool staleCacheData; TableDescription table = tableInfoCache.GetValue(TableName, this.DescribeTable, out staleCacheData); if (staleCacheData) { var logger = Logger.GetLogger(typeof(Table)); logger.InfoFormat("Description for table [{0}] loaded from SDK Cache", TableName); } foreach (var key in table.KeySchema) { string keyName = key.AttributeName; AttributeDefinition attributeDefinition = table.AttributeDefinitions .FirstOrDefault(a => string.Equals(a.AttributeName, keyName, StringComparison.Ordinal)); if (attributeDefinition == null) throw new InvalidOperationException("No attribute definition found for key " + key.AttributeName); KeyDescription keyDescription = new KeyDescription { IsHash = string.Equals(key.KeyType, "HASH", StringComparison.OrdinalIgnoreCase), Type = GetType(attributeDefinition.AttributeType) }; if (keyDescription.IsHash) HashKeys.Add(keyName); else RangeKeys.Add(keyName); Keys[keyName] = keyDescription; } if (table.LocalSecondaryIndexes != null) { foreach (var index in table.LocalSecondaryIndexes) { LocalSecondaryIndexes[index.IndexName] = index; LocalSecondaryIndexNames.Add(index.IndexName); } } if (table.GlobalSecondaryIndexes != null) { foreach (var index in table.GlobalSecondaryIndexes) { GlobalSecondaryIndexes[index.IndexName] = index; GlobalSecondaryIndexNames.Add(index.IndexName); } } foreach (var attribute in table.AttributeDefinitions) { Attributes.Add(attribute); } } internal Key MakeKey(IDictionary doc) { Key key = new Key(); foreach (var kvp in Keys) { string keyName = kvp.Key; KeyDescription description = kvp.Value; DynamoDBEntry value; if (!doc.TryGetValue(keyName, out value)) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Document does not contain value for key {0}", keyName)); value = value.ToConvertedEntry(Conversion); if (StoreAsEpoch.Contains(keyName)) value = Document.DateTimeToEpochSeconds(value, keyName); Primitive primitive = value.AsPrimitive(); if (primitive == null) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Key attribute {0} must be a Primitive type", keyName)); if (primitive.Type != description.Type) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Key attribute {0} must be of type {1}", keyName, description.Type)); var attributeConversionConfig = new DynamoDBEntry.AttributeConversionConfig(Conversion, IsEmptyStringValueEnabled); key[keyName] = primitive.ConvertToAttributeValue(attributeConversionConfig); } return key; } internal Key MakeKey(Primitive hashKey, Primitive rangeKey) { Key newKey = new Key(); if (HashKeys.Count != 1) throw new InvalidOperationException("Must have one hash key defined for the table " + TableName); string hashKeyName = HashKeys[0]; KeyDescription hashKeyDescription = Keys[hashKeyName]; if (this.StoreAsEpoch.Contains(hashKeyName)) hashKey = KeyDateTimeToEpochSeconds(hashKey, hashKeyName); if (hashKeyDescription.Type != hashKey.Type) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Schema for table {0}, hash key {1}, is inconsistent with specified hash key value.", TableName, hashKeyName)); var hashKeyAttributeConversionConfig = new DynamoDBEntry.AttributeConversionConfig(Conversion, IsEmptyStringValueEnabled); var hashKeyAttributeValue = hashKey.ConvertToAttributeValue(hashKeyAttributeConversionConfig); newKey[hashKeyName] = hashKeyAttributeValue; if ((rangeKey == null) != (RangeKeys.Count == 0)) { throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Schema for table {0}, range key {1}, is inconsistent with specified range key value.", TableName, hashKeyName)); } else if (rangeKey != null) { string rangeKeyName = RangeKeys[0]; KeyDescription rangeKeyDescription = Keys[rangeKeyName]; if (this.StoreAsEpoch.Contains(rangeKeyName)) rangeKey = KeyDateTimeToEpochSeconds(rangeKey, rangeKeyName); if (rangeKeyDescription.Type != rangeKey.Type) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Schema for table {0}, range key {1}, is inconsistent with specified range key value.", TableName, hashKeyName)); var rangeKeyAttributeConversionConfig = new DynamoDBEntry.AttributeConversionConfig(Conversion, IsEmptyStringValueEnabled); var rangeKeyAttributeValue = rangeKey.ConvertToAttributeValue(rangeKeyAttributeConversionConfig); newKey[rangeKeyName] = rangeKeyAttributeValue; } return newKey; } private static Primitive KeyDateTimeToEpochSeconds(Primitive key, string attributeName) { DynamoDBEntry entry = Document.DateTimeToEpochSeconds(key, attributeName); Primitive converted = entry as Primitive; return converted; } internal void UserAgentRequestEventHandlerSync(object sender, RequestEventArgs args) { UserAgentRequestEventHandler(sender, args, false); } internal void UserAgentRequestEventHandlerAsync(object sender, RequestEventArgs args) { UserAgentRequestEventHandler(sender, args, true); } internal void AddRequestHandler(AmazonDynamoDBRequest request, bool isAsync) { ((Amazon.Runtime.Internal.IAmazonWebServiceRequest)request).AddBeforeRequestHandler(isAsync ? new RequestEventHandler(this.UserAgentRequestEventHandlerAsync) : new RequestEventHandler(this.UserAgentRequestEventHandlerSync) ); } private void UserAgentRequestEventHandler(object sender, RequestEventArgs args, bool isAsync) { WebServiceRequestEventArgs wsArgs = args as WebServiceRequestEventArgs; if (wsArgs != null) { if (wsArgs.Headers.Keys.Contains(HeaderKeys.UserAgentHeader)) { string currentUserAgent = wsArgs.Headers[HeaderKeys.UserAgentHeader]; wsArgs.Headers[HeaderKeys.UserAgentHeader] = currentUserAgent + " " + this.TableConsumer.ToString() + " " + (isAsync ? "TableAsync" : "TableSync"); } else if (wsArgs.Headers.Keys.Contains(HeaderKeys.XAmzUserAgentHeader)) { string currentUserAgent = wsArgs.Headers[HeaderKeys.XAmzUserAgentHeader]; wsArgs.Headers[HeaderKeys.XAmzUserAgentHeader] = currentUserAgent + " " + this.TableConsumer.ToString() + " " + (isAsync ? "TableAsync" : "TableSync"); } } } /// /// Validates that the conditional properties on the config object are correctly set. /// /// /// private static void ValidateConditional(IConditionalOperationConfig config) { if (config == null) return; int conditionsSet = 0; conditionsSet += config.Expected != null ? 1 : 0; conditionsSet += config.ExpectedState != null ? 1 : 0; conditionsSet += config.ConditionalExpression != null && config.ConditionalExpression.ExpressionStatement != null ? 1 : 0; if (conditionsSet > 1) throw new InvalidOperationException("Only one of the conditonal properties Expected, ExpectedState and ConditionalExpression can be set."); } private void ClearTableData() { Keys = new Dictionary(); HashKeys = new List(); RangeKeys = new List(); LocalSecondaryIndexes = new Dictionary(); LocalSecondaryIndexNames = new List(); GlobalSecondaryIndexes = new Dictionary(); GlobalSecondaryIndexNames = new List(); Attributes = new List(); } private TableDescription DescribeTable(string tableName) { DescribeTableRequest req = new DescribeTableRequest { TableName = TableName }; this.AddRequestHandler(req, isAsync: false); var info = this.DDBClient.DescribeTable(req); if (info.Table == null) { throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Table name {0} does not exist", TableName)); } return info.Table; } internal Table Copy() { return this.Copy(Config); } internal Table Copy(TableConfig newConfig) { return new Table(this.DDBClient, newConfig) { Keys = this.Keys, RangeKeys = this.RangeKeys, HashKeys = this.HashKeys, Attributes = this.Attributes, GlobalSecondaryIndexes = this.GlobalSecondaryIndexes, GlobalSecondaryIndexNames = this.GlobalSecondaryIndexNames, LocalSecondaryIndexes = this.LocalSecondaryIndexes, LocalSecondaryIndexNames = this.LocalSecondaryIndexNames }; } internal Dictionary ToAttributeMap(Document doc, DynamoDBEntryConversion conversion) { return doc.ToAttributeMap(conversion, this.StoreAsEpoch, Config.IsEmptyStringValueEnabled); } #endregion #region Constructor/factory private Table(IAmazonDynamoDB ddbClient, TableConfig config) { if (config == null) throw new ArgumentNullException("config"); if (ddbClient == null) throw new ArgumentNullException("ddbClient"); #if NETSTANDARD DDBClient = ddbClient as AmazonDynamoDBClient; #else DDBClient = ddbClient; #endif Config = config; } /// /// Clears current table cache. Next time a Table is created, its information /// will be loaded from DynamoDB. /// public static void ClearTableCache() { SdkCache.Clear(TableInfoCacheIdentifier); } /// /// Creates a Table object with the specified configuration, using the /// passed-in client to load the table definition. /// /// This method will throw an exception if the table does not exist. /// /// Client to use to access DynamoDB. /// Configuration to use for the table. /// Table object representing the specified table. public static Table LoadTable(IAmazonDynamoDB ddbClient, TableConfig config) { Table table = new Table(ddbClient, config); table.LoadTableInfo(); return table; } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// The returned table will use the conversion specified by /// AWSConfigs.DynamoDBConfig.ConversionSchema /// /// This method will throw an exception if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// Table object representing the specified table. public static Table LoadTable(IAmazonDynamoDB ddbClient, string tableName) { return LoadTable(ddbClient, tableName, DynamoDBEntryConversion.CurrentConversion, false); } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// /// This method will throw an exception if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// Conversion to use for converting .NET values to DynamoDB values. /// Table object representing the specified table. public static Table LoadTable(IAmazonDynamoDB ddbClient, string tableName, DynamoDBEntryConversion conversion) { return LoadTable(ddbClient, tableName, conversion, false); } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// /// This method will throw an exception if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// If the property is false, empty string values will be interpreted as null values. /// Table object representing the specified table. public static Table LoadTable(IAmazonDynamoDB ddbClient, string tableName, bool isEmptyStringValueEnabled) { return LoadTable(ddbClient, tableName, DynamoDBEntryConversion.CurrentConversion, isEmptyStringValueEnabled); } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// /// This method will throw an exception if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// Conversion to use for converting .NET values to DynamoDB values. /// If the property is false, empty string values will be interpreted as null values. /// Table object representing the specified table. public static Table LoadTable(IAmazonDynamoDB ddbClient, string tableName, DynamoDBEntryConversion conversion, bool isEmptyStringValueEnabled) { var config = new TableConfig(tableName, conversion, DynamoDBConsumer.DocumentModel, storeAsEpoch: null, isEmptyStringValueEnabled: isEmptyStringValueEnabled); return LoadTable(ddbClient, config); } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// The returned table will use the conversion specified by /// AWSConfigs.DynamoDBConfig.ConversionSchema /// /// This method will return false if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// Loaded table. /// /// True if table was successfully loaded; otherwise false. /// public static bool TryLoadTable(IAmazonDynamoDB ddbClient, string tableName, out Table table) { return TryLoadTable(ddbClient, tableName, DynamoDBEntryConversion.CurrentConversion, false, out table); } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// /// This method will return false if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// Conversion to use for converting .NET values to DynamoDB values. /// Loaded table. /// /// True if table was successfully loaded; otherwise false. /// public static bool TryLoadTable(IAmazonDynamoDB ddbClient, string tableName, DynamoDBEntryConversion conversion, out Table table) { return TryLoadTable(ddbClient, tableName, conversion, false, out table); } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// /// This method will return false if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// If the property is false, empty string values will be interpreted as null values. /// Loaded table. /// /// True if table was successfully loaded; otherwise false. /// public static bool TryLoadTable(IAmazonDynamoDB ddbClient, string tableName, bool isEmptyStringValueEnabled, out Table table) { return TryLoadTable(ddbClient, tableName, DynamoDBEntryConversion.CurrentConversion, isEmptyStringValueEnabled, out table); } /// /// Creates a Table object with the specified name, using the /// passed-in client to load the table definition. /// /// This method will return false if the table does not exist. /// /// Client to use to access DynamoDB. /// Name of the table. /// Conversion to use for converting .NET values to DynamoDB values. /// If the property is false, empty string values will be interpreted as null values. /// Loaded table. /// /// True if table was successfully loaded; otherwise false. /// public static bool TryLoadTable(IAmazonDynamoDB ddbClient, string tableName, DynamoDBEntryConversion conversion, bool isEmptyStringValueEnabled, out Table table) { var config = new TableConfig(tableName, conversion, DynamoDBConsumer.DocumentModel, storeAsEpoch: null, isEmptyStringValueEnabled: isEmptyStringValueEnabled); return TryLoadTable(ddbClient, config, out table); } /// /// Creates a Table object with the specified configuration, using the /// passed-in client to load the table definition. /// /// This method will return false if the table does not exist. /// /// Client to use to access DynamoDB. /// Configuration to use for the table. /// Loaded table. /// /// True if table was successfully loaded; otherwise false. /// public static bool TryLoadTable(IAmazonDynamoDB ddbClient, TableConfig config, out Table table) { if (config == null) throw new ArgumentNullException("config"); try { table = LoadTable(ddbClient, config); return true; } catch { table = null; return false; } } #endregion #region Conversion methods /// /// Creates a Document from an attribute map. /// /// Map of attribute names to attribute values. /// Document representing the data. public Document FromAttributeMap(Dictionary data) { return Document.FromAttributeMap(data, this.StoreAsEpoch); } /// /// Creates a map of attribute names mapped to AttributeValue objects. /// Converts .NET types using the conversion specified in this Table. /// /// public Dictionary ToAttributeMap(Document doc) { return doc.ToAttributeMap(this.Conversion, this.StoreAsEpoch, this.IsEmptyStringValueEnabled); } /// /// Creates a map of attribute names mapped to ExpectedAttributeValue objects. /// /// public Dictionary ToExpectedAttributeMap(Document doc) { return doc.ToExpectedAttributeMap(this.Conversion, this.StoreAsEpoch, this.IsEmptyStringValueEnabled); } /// /// Creates a map of attribute names mapped to AttributeValueUpdate objects. /// /// /// If true, only attributes that have been changed will be in the map. /// public Dictionary ToAttributeUpdateMap(Document doc, bool changedAttributesOnly) { return doc.ToAttributeUpdateMap(this.Conversion, changedAttributesOnly, this.StoreAsEpoch, this.IsEmptyStringValueEnabled); } #endregion #region PutItem internal Document PutItemHelper(Document doc, PutItemOperationConfig config) { var currentConfig = config ?? new PutItemOperationConfig(); PutItemRequest req = new PutItemRequest { TableName = TableName, Item = this.ToAttributeMap(doc) }; this.AddRequestHandler(req, isAsync: false); if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) req.ReturnValues = EnumMapper.Convert(currentConfig.ReturnValues); ValidateConditional(currentConfig); if (currentConfig.Expected != null) { req.Expected = this.ToExpectedAttributeMap(currentConfig.Expected); } else if (currentConfig.ExpectedState != null && currentConfig.ExpectedState.ExpectedValues != null && currentConfig.ExpectedState.ExpectedValues.Count > 0) { req.Expected = currentConfig.ExpectedState.ToExpectedAttributeMap(this); if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) { currentConfig.ConditionalExpression.ApplyExpression(req, this); } var resp = DDBClient.PutItem(req); doc.CommitChanges(); Document ret = null; if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) { ret = this.FromAttributeMap(resp.Attributes); } return ret; } #if AWS_ASYNC_API internal async Task PutItemHelperAsync(Document doc, PutItemOperationConfig config, CancellationToken cancellationToken) { var currentConfig = config ?? new PutItemOperationConfig(); PutItemRequest req = new PutItemRequest { TableName = TableName, Item = this.ToAttributeMap(doc) }; this.AddRequestHandler(req, isAsync: true); if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) req.ReturnValues = EnumMapper.Convert(currentConfig.ReturnValues); ValidateConditional(currentConfig); if (currentConfig.Expected != null) { req.Expected = this.ToExpectedAttributeMap(currentConfig.Expected); } else if (currentConfig.ExpectedState != null && currentConfig.ExpectedState.ExpectedValues != null && currentConfig.ExpectedState.ExpectedValues.Count > 0) { req.Expected = currentConfig.ExpectedState.ToExpectedAttributeMap(this); if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) { currentConfig.ConditionalExpression.ApplyExpression(req, this); } var resp = await DDBClient.PutItemAsync(req, cancellationToken).ConfigureAwait(false); doc.CommitChanges(); Document ret = null; if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) { ret = this.FromAttributeMap(resp.Attributes); } return ret; } #endif #endregion #region GetItem internal Document GetItemHelper(Key key, GetItemOperationConfig config) { var currentConfig = config ?? new GetItemOperationConfig(); var request = new GetItemRequest { TableName = TableName, Key = key, ConsistentRead = currentConfig.ConsistentRead }; this.AddRequestHandler(request, isAsync: false); if (currentConfig.AttributesToGet != null) request.AttributesToGet = currentConfig.AttributesToGet; var result = DDBClient.GetItem(request); var attributeMap = result.Item; if (attributeMap == null || attributeMap.Count == 0) return null; return this.FromAttributeMap(attributeMap); } #if AWS_ASYNC_API internal async Task GetItemHelperAsync(Key key, GetItemOperationConfig config, CancellationToken cancellationToken) { var currentConfig = config ?? new GetItemOperationConfig(); var request = new GetItemRequest { TableName = TableName, Key = key, ConsistentRead = currentConfig.ConsistentRead }; this.AddRequestHandler(request, isAsync: true); if (currentConfig.AttributesToGet != null) request.AttributesToGet = currentConfig.AttributesToGet; var result = await DDBClient.GetItemAsync(request, cancellationToken).ConfigureAwait(false); var attributeMap = result.Item; if (attributeMap == null || attributeMap.Count == 0) return null; return this.FromAttributeMap(attributeMap); } #endif #endregion #region UpdateItem internal Document UpdateHelper(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config) { Key key = (hashKey != null || rangeKey != null) ? MakeKey(hashKey, rangeKey) : MakeKey(doc); return UpdateHelper(doc, key, config); } #if AWS_ASYNC_API internal Task UpdateHelperAsync(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config, CancellationToken cancellationToken) { Key key = (hashKey != null || rangeKey != null) ? MakeKey(hashKey, rangeKey) : MakeKey(doc); return UpdateHelperAsync(doc, key, config, cancellationToken); } #endif internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig config) { var currentConfig = config ?? new UpdateItemOperationConfig(); // If the keys have been changed, treat entire document as having changed bool haveKeysChanged = HaveKeysChanged(doc); bool updateChangedAttributesOnly = !haveKeysChanged; var attributeUpdates = this.ToAttributeUpdateMap(doc, updateChangedAttributesOnly); foreach (var keyName in this.KeyNames) { attributeUpdates.Remove(keyName); } UpdateItemRequest req = new UpdateItemRequest { TableName = TableName, Key = key, AttributeUpdates = attributeUpdates.Count == 0 ? null : attributeUpdates, // pass null if keys-only update ReturnValues = EnumMapper.Convert(currentConfig.ReturnValues) }; this.AddRequestHandler(req, isAsync: false); ValidateConditional(currentConfig); if (currentConfig.Expected != null) { req.Expected = this.ToExpectedAttributeMap(currentConfig.Expected); } else if (currentConfig.ExpectedState != null && currentConfig.ExpectedState.ExpectedValues != null && currentConfig.ExpectedState.ExpectedValues.Count > 0) { req.Expected = currentConfig.ExpectedState.ToExpectedAttributeMap(this); if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) { currentConfig.ConditionalExpression.ApplyExpression(req, this); string statement; Dictionary expressionAttributeValues; Dictionary expressionAttributeNames; Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, out statement, out expressionAttributeValues, out expressionAttributeNames); req.AttributeUpdates = null; req.UpdateExpression = statement; if (req.ExpressionAttributeValues == null) req.ExpressionAttributeValues = expressionAttributeValues; else { foreach (var kvp in expressionAttributeValues) req.ExpressionAttributeValues.Add(kvp.Key, kvp.Value); } if (req.ExpressionAttributeNames == null) req.ExpressionAttributeNames = expressionAttributeNames; else { foreach (var kvp in expressionAttributeNames) req.ExpressionAttributeNames.Add(kvp.Key, kvp.Value); } } var resp = DDBClient.UpdateItem(req); var returnedAttributes = resp.Attributes; doc.CommitChanges(); Document ret = null; if (currentConfig.ReturnValues != ReturnValues.None) { ret = this.FromAttributeMap(returnedAttributes); } return ret; } #if AWS_ASYNC_API internal async Task UpdateHelperAsync(Document doc, Key key, UpdateItemOperationConfig config, CancellationToken cancellationToken) { var currentConfig = config ?? new UpdateItemOperationConfig(); // If the keys have been changed, treat entire document as having changed bool haveKeysChanged = HaveKeysChanged(doc); bool updateChangedAttributesOnly = !haveKeysChanged; var attributeUpdates = this.ToAttributeUpdateMap(doc, updateChangedAttributesOnly); foreach (var keyName in this.KeyNames) { attributeUpdates.Remove(keyName); } UpdateItemRequest req = new UpdateItemRequest { TableName = TableName, Key = key, AttributeUpdates = attributeUpdates.Count == 0 ? null : attributeUpdates, // pass null if keys-only update ReturnValues = EnumMapper.Convert(currentConfig.ReturnValues) }; this.AddRequestHandler(req, isAsync: true); ValidateConditional(currentConfig); if (currentConfig.Expected != null) { req.Expected = this.ToExpectedAttributeMap(currentConfig.Expected); } else if (currentConfig.ExpectedState != null && currentConfig.ExpectedState.ExpectedValues != null && currentConfig.ExpectedState.ExpectedValues.Count > 0) { req.Expected = currentConfig.ExpectedState.ToExpectedAttributeMap(this); if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) { currentConfig.ConditionalExpression.ApplyExpression(req, this); string statement; Dictionary expressionAttributeValues; Dictionary expressionAttributeNames; Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdates, out statement, out expressionAttributeValues, out expressionAttributeNames); req.AttributeUpdates = null; req.UpdateExpression = statement; if (req.ExpressionAttributeValues == null) req.ExpressionAttributeValues = expressionAttributeValues; else { foreach (var kvp in expressionAttributeValues) req.ExpressionAttributeValues.Add(kvp.Key, kvp.Value); } if (req.ExpressionAttributeNames == null) req.ExpressionAttributeNames = expressionAttributeNames; else { foreach (var kvp in expressionAttributeNames) req.ExpressionAttributeNames.Add(kvp.Key, kvp.Value); } } var resp = await DDBClient.UpdateItemAsync(req, cancellationToken).ConfigureAwait(false); var returnedAttributes = resp.Attributes; doc.CommitChanges(); Document ret = null; if (currentConfig.ReturnValues != ReturnValues.None) { ret = this.FromAttributeMap(returnedAttributes); } return ret; } #endif // Checks if key attributes have been updated internal bool HaveKeysChanged(Document doc) { foreach (var keyName in this.KeyNames) { if (doc.IsAttributeChanged(keyName)) return true; } return false; } #endregion #region DeleteItem internal Document DeleteHelper(Key key, DeleteItemOperationConfig config) { var currentConfig = config ?? new DeleteItemOperationConfig(); var req = new DeleteItemRequest { TableName = TableName, Key = key }; this.AddRequestHandler(req, isAsync: false); if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) req.ReturnValues = EnumMapper.Convert(currentConfig.ReturnValues); ValidateConditional(currentConfig); if (currentConfig.Expected != null) { req.Expected = this.ToExpectedAttributeMap(currentConfig.Expected); } else if (currentConfig.ExpectedState != null && currentConfig.ExpectedState.ExpectedValues != null && currentConfig.ExpectedState.ExpectedValues.Count > 0) { req.Expected = currentConfig.ExpectedState.ToExpectedAttributeMap(this); if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) { currentConfig.ConditionalExpression.ApplyExpression(req, this); } var attributes = DDBClient.DeleteItem(req).Attributes; Document ret = null; if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) { ret = this.FromAttributeMap(attributes); } return ret; } #if AWS_ASYNC_API internal async Task DeleteHelperAsync(Key key, DeleteItemOperationConfig config, CancellationToken cancellationToken) { var currentConfig = config ?? new DeleteItemOperationConfig(); var req = new DeleteItemRequest { TableName = TableName, Key = key }; this.AddRequestHandler(req, isAsync: true); if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) req.ReturnValues = EnumMapper.Convert(currentConfig.ReturnValues); ValidateConditional(currentConfig); if (currentConfig.Expected != null) { req.Expected = this.ToExpectedAttributeMap(currentConfig.Expected); } else if (currentConfig.ExpectedState != null && currentConfig.ExpectedState.ExpectedValues != null && currentConfig.ExpectedState.ExpectedValues.Count > 0) { req.Expected = currentConfig.ExpectedState.ToExpectedAttributeMap(this); if (req.Expected.Count > 1) req.ConditionalOperator = EnumMapper.Convert(currentConfig.ExpectedState.ConditionalOperator); } else if (currentConfig.ConditionalExpression != null && currentConfig.ConditionalExpression.IsSet) { currentConfig.ConditionalExpression.ApplyExpression(req, this); } var attributes = (await DDBClient.DeleteItemAsync(req, cancellationToken).ConfigureAwait(false)).Attributes; Document ret = null; if (currentConfig.ReturnValues == ReturnValues.AllOldAttributes) { ret = this.FromAttributeMap(attributes); } return ret; } #endif #endregion #region Scan /// /// Initiates a Search object to Scan a DynamoDB table, with the /// specified filter. /// /// No calls are made until the Search object is used. /// /// Filter to apply to the scan. /// Resultant Search container. public Search Scan(ScanFilter filter) { return Scan(new ScanOperationConfig { Filter = filter }); } /// /// Initiates a Search object to Scan a DynamoDB table, with the /// specified expression. /// /// No calls are made until the Search object is used. /// /// Expression to apply to the scan. /// Resultant Search container. public Search Scan(Expression filterExpression) { ScanOperationConfig config = new ScanOperationConfig { FilterExpression = filterExpression }; return Scan(config); } /// /// Initiates a Search object to Scan a DynamoDB table, with the /// specified config. /// /// No calls are made until the Search object is used. /// /// Configuration to use. /// Resultant Search container. public Search Scan(ScanOperationConfig config) { var currentConfig = config ?? new ScanOperationConfig(); Search ret = new Search(SearchType.Scan) { SourceTable = this, TableName = TableName, Limit = currentConfig.Limit, Filter = currentConfig.Filter, FilterExpression = currentConfig.FilterExpression, ConditionalOperator = currentConfig.ConditionalOperator, AttributesToGet = currentConfig.AttributesToGet, Select = currentConfig.Select, CollectResults = currentConfig.CollectResults, IndexName = currentConfig.IndexName, IsConsistentRead = currentConfig.ConsistentRead, PaginationToken = currentConfig.PaginationToken }; if (currentConfig.TotalSegments != 0) { ret.TotalSegments = currentConfig.TotalSegments; ret.Segment = currentConfig.Segment; } return ret; } #endregion #region Query /// /// Initiates a Search object to Query a DynamoDB table, with the /// specified hash primary key and filter. /// /// No calls are made until the Search object is used. /// /// Value of the hash key for the query operation. /// Filter to use. /// Resultant Search container. public Search Query(Primitive hashKey, QueryFilter filter) { string hashKeyName = this.HashKeys[0]; QueryFilter fullFilter = new QueryFilter(filter); fullFilter.AddCondition(hashKeyName, QueryOperator.Equal, hashKey); return Query(fullFilter); } /// /// Initiates a Search object to Query a DynamoDB table, with the /// specified hash primary key and expression. /// /// No calls are made until the Search object is used. /// /// Value of the hash key for the query operation. /// Expression to use. /// Resultant Search container. public Search Query(Primitive hashKey, Expression filterExpression) { string hashKeyName = this.HashKeys[0]; QueryFilter hashKeyFilter = new QueryFilter(); hashKeyFilter.AddCondition(hashKeyName, QueryOperator.Equal, hashKey); QueryOperationConfig config = new QueryOperationConfig { Filter = hashKeyFilter, FilterExpression = filterExpression }; return Query(config); } /// /// Initiates a Search object to Query a DynamoDB table, with the /// specified filter. /// /// No calls are made until the Search object is used. /// /// Filter to use. /// Resultant Search container. public Search Query(QueryFilter filter) { return Query(new QueryOperationConfig { Filter = filter }); } /// /// Initiates a Search object to Query a DynamoDB table, with the /// specified config. /// /// No calls are made until the Search object is used. /// /// Configuration to use. /// Resultant Search container. public Search Query(QueryOperationConfig config) { if (config == null) throw new ArgumentNullException("config"); Search ret = new Search(SearchType.Query) { SourceTable = this, TableName = TableName, AttributesToGet = config.AttributesToGet, Filter = config.Filter, KeyExpression = config.KeyExpression, FilterExpression = config.FilterExpression, ConditionalOperator = config.ConditionalOperator, Limit = config.Limit, IsConsistentRead = config.ConsistentRead, IsBackwardSearch = config.BackwardSearch, IndexName = config.IndexName, Select = config.Select, CollectResults = config.CollectResults, PaginationToken = config.PaginationToken }; return ret; } #endregion #region BatchGet /// /// Creates a DocumentBatchGet object for the current table, allowing /// a batch-get operation against DynamoDB. /// /// Empty DocumentBatchGet object. public DocumentBatchGet CreateBatchGet() { return new DocumentBatchGet(this); } #endregion #region BatchWrite /// /// Creates a DocumentBatchWrite object for the current table, allowing /// a batch-put/delete operation against DynamoDB. /// /// Empty DocumentBatchWrite object. public DocumentBatchWrite CreateBatchWrite() { return new DocumentBatchWrite(this); } #endregion #region TransactGet /// /// Creates a DocumentTransactGet object for the current table, allowing /// a transactional get operation against DynamoDB. /// /// Empty DocumentTransactGet object. public DocumentTransactGet CreateTransactGet() { return new DocumentTransactGet(this); } #endregion #region TransactWrite /// /// Creates a DocumentTransactWrite object for the current table, allowing /// a transactional condition-check/put/update/delete operation against DynamoDB. /// /// Empty DocumentTransactWrite object. public DocumentTransactWrite CreateTransactWrite() { return new DocumentTransactWrite(this); } #endregion } }