/// <summary> /// Inserts or updates one or more items into the Cosmos DB container. /// </summary> /// <param name="changes">A dictionary of items to be inserted or updated. The dictionary item key /// is used as the ID for the inserted / updated item.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A Task representing the work to be executed.</returns> /// <exception cref="ArgumentNullException">Exception thrown if the changes dictionary is null.</exception> /// <exception cref="Exception">Exception thrown is the etag is empty on any of the items within the changes dictionary.</exception> public async Task WriteAsync(IDictionary <string, object> changes, CancellationToken cancellationToken = default(CancellationToken)) { if (changes == null) { throw new ArgumentNullException(nameof(changes)); } if (changes.Count == 0) { return; } // Ensure Initialization has been run await InitializeAsync().ConfigureAwait(false); foreach (var change in changes) { var json = JObject.FromObject(change.Value, _jsonSerializer); // Remove etag from JSON object that was copied from IStoreItem. // The ETag information is updated as an _etag attribute in the document metadata. json.Remove("eTag"); var documentChange = new DocumentStoreItem { Id = CosmosDbKeyEscape.EscapeKey(change.Key, _cosmosDbStorageOptions.KeySuffix, _cosmosDbStorageOptions.CompatibilityMode), RealId = change.Key, Document = json, }; var etag = (change.Value as IStoreItem)?.ETag; if (etag == null || etag == "*") { // if new item or * then insert or replace unconditionally await _container.UpsertItemAsync( documentChange, GetPartitionKey(documentChange.PartitionKey), cancellationToken : cancellationToken) .ConfigureAwait(false); } else if (etag.Length > 0) { // if we have an etag, do opt. concurrency replace await _container.UpsertItemAsync( documentChange, GetPartitionKey(documentChange.PartitionKey), new ItemRequestOptions() { IfMatchEtag = etag, }, cancellationToken) .ConfigureAwait(false); } else { throw new ArgumentException("etag empty"); } } }
/// <summary> /// Reads one or more items with matching keys from the Cosmos DB container. /// </summary> /// <param name="keys">A collection of Ids for each item to be retrieved.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> /// <returns>A dictionary containing the retrieved items.</returns> /// <exception cref="ArgumentNullException">Exception thrown if the array of keys (Ids for the items to be retrieved) is null.</exception> public async Task <IDictionary <string, object> > ReadAsync(string[] keys, CancellationToken cancellationToken = default(CancellationToken)) { if (keys == null) { throw new ArgumentNullException(nameof(keys)); } if (keys.Length == 0) { // No keys passed in, no result to return. return(new Dictionary <string, object>()); } // Ensure Initialization has been run await InitializeAsync().ConfigureAwait(false); var storeItems = new Dictionary <string, object>(keys.Length); foreach (var key in keys) { try { var escapedKey = CosmosDbKeyEscape.EscapeKey(key, _cosmosDbStorageOptions.KeySuffix, _cosmosDbStorageOptions.CompatibilityMode); var readItemResponse = await _container.ReadItemAsync <DocumentStoreItem>( escapedKey, GetPartitionKey(escapedKey), cancellationToken : cancellationToken) .ConfigureAwait(false); var documentStoreItem = readItemResponse.Resource; var item = documentStoreItem.Document.ToObject(typeof(object), _jsonSerializer); if (item is IStoreItem storeItem) { storeItem.ETag = documentStoreItem.ETag; storeItems.Add(documentStoreItem.RealId, storeItem); } else { storeItems.Add(documentStoreItem.RealId, item); } } catch (CosmosException exception) { // When an item is not found a CosmosException is thrown, but we want to // return an empty collection so in this instance we catch and do not rethrow. // Throw for any other exception. if (exception.StatusCode == HttpStatusCode.NotFound) { break; } throw; } } return(storeItems); }
/// <summary> /// Writes storage items to storage. /// </summary> /// <param name="changes">The items to write to storage, indexed by key.</param> /// <param name="cancellationToken">A cancellation token that can be used by other objects /// or threads to receive notice of cancellation.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <seealso cref="DeleteAsync(string[], CancellationToken)"/> /// <seealso cref="ReadAsync(string[], CancellationToken)"/> public async Task WriteAsync(IDictionary <string, object> changes, CancellationToken cancellationToken) { if (changes == null) { throw new ArgumentNullException(nameof(changes)); } if (changes.Count == 0) { return; } // Ensure Initialization has been run await InitializeAsync().ConfigureAwait(false); foreach (var change in changes) { var json = JObject.FromObject(change.Value, _jsonSerializer); // Remove etag from JSON object that was copied from IStoreItem. // The ETag information is updated as an _etag attribute in the document metadata. json.Remove("eTag"); var documentChange = new DocumentStoreItem { Id = CosmosDbKeyEscape.EscapeKey(change.Key), ReadlId = change.Key, Document = json, }; var etag = (change.Value as IStoreItem)?.ETag; if (etag == null || etag == "*") { // if new item or * then insert or replace unconditionaly await _client.UpsertDocumentAsync( _collectionLink, documentChange, disableAutomaticIdGeneration : true, cancellationToken : cancellationToken).ConfigureAwait(false); } else if (etag.Length > 0) { // if we have an etag, do opt. concurrency replace var uri = UriFactory.CreateDocumentUri(_databaseId, _collectionId, documentChange.Id); var ac = new AccessCondition { Condition = etag, Type = AccessConditionType.IfMatch }; await _client.ReplaceDocumentAsync( uri, documentChange, new RequestOptions { AccessCondition = ac }, cancellationToken : cancellationToken).ConfigureAwait(false); } else { throw new ArgumentException("etag empty"); } } }
/// <summary> /// Initializes a new instance of the <see cref="CosmosDbPartitionedStorage"/> class. /// using the provided CosmosDB credentials, database ID, and container ID. /// </summary> /// <param name="cosmosDbStorageOptions">Cosmos DB partitioned storage configuration options.</param> public CosmosDbPartitionedStorage(CosmosDbPartitionedStorageOptions cosmosDbStorageOptions) { if (cosmosDbStorageOptions == null) { throw new ArgumentNullException(nameof(cosmosDbStorageOptions)); } if (cosmosDbStorageOptions.CosmosDbEndpoint == null) { throw new ArgumentException($"Service EndPoint for CosmosDB is required.", nameof(cosmosDbStorageOptions)); } if (string.IsNullOrEmpty(cosmosDbStorageOptions.AuthKey)) { throw new ArgumentException("AuthKey for CosmosDB is required.", nameof(cosmosDbStorageOptions)); } if (string.IsNullOrEmpty(cosmosDbStorageOptions.DatabaseId)) { throw new ArgumentException("DatabaseId is required.", nameof(cosmosDbStorageOptions)); } if (string.IsNullOrEmpty(cosmosDbStorageOptions.ContainerId)) { throw new ArgumentException("ContainerId is required.", nameof(cosmosDbStorageOptions)); } if (!string.IsNullOrWhiteSpace(cosmosDbStorageOptions.KeySuffix)) { if (cosmosDbStorageOptions.CompatibilityMode) { throw new ArgumentException($"CompatibilityMode cannot be 'true' while using a KeySuffix.", nameof(cosmosDbStorageOptions)); } // In order to reduce key complexity, we do not allow invalid characters in a KeySuffix // If the KeySuffix has invalid characters, the EscapeKey will not match var suffixEscaped = CosmosDbKeyEscape.EscapeKey(cosmosDbStorageOptions.KeySuffix); if (!cosmosDbStorageOptions.KeySuffix.Equals(suffixEscaped, StringComparison.Ordinal)) { throw new ArgumentException($"Cannot use invalid Row Key characters: {cosmosDbStorageOptions.KeySuffix}", nameof(cosmosDbStorageOptions)); } } _cosmosDbStorageOptions = cosmosDbStorageOptions; }
public async Task DeleteAsync(string[] keys, CancellationToken cancellationToken = default(CancellationToken)) { if (keys == null) { throw new ArgumentNullException(nameof(keys)); } await InitializeAsync().ConfigureAwait(false); foreach (var key in keys) { await _container.DeleteItemAsync <DocumentStoreItem>( partitionKey : new PartitionKey(key), id : CosmosDbKeyEscape.EscapeKey(key), cancellationToken : cancellationToken) .ConfigureAwait(false); } }
/// <summary> /// Deletes storage items from storage. /// </summary> /// <param name="keys">keys of the <see cref="IStoreItem"/> objects to remove from the store.</param> /// <param name="cancellationToken">A cancellation token that can be used by other objects /// or threads to receive notice of cancellation.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <seealso cref="ReadAsync(string[], CancellationToken)"/> /// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/> public async Task DeleteAsync(string[] keys, CancellationToken cancellationToken) { RequestOptions options = null; if (keys == null) { throw new ArgumentNullException(nameof(keys)); } if (keys.Length == 0) { return; } // Ensure Initialization has been run await InitializeAsync().ConfigureAwait(false); if (!string.IsNullOrEmpty(this._partitionKey)) { options = new RequestOptions() { PartitionKey = new PartitionKey(this._partitionKey) }; } // Parallelize deletion var tasks = keys.Select(key => _client.DeleteDocumentAsync( UriFactory.CreateDocumentUri( _databaseId, _collectionId, CosmosDbKeyEscape.EscapeKey(key)), options, cancellationToken: cancellationToken)); // await to deletion tasks to complete await Task.WhenAll(tasks).ConfigureAwait(false); }
/// <summary> /// Reads storage items from storage. /// </summary> /// <param name="keys">keys of the <see cref="IStoreItem"/> objects to read from the store.</param> /// <param name="cancellationToken">A cancellation token that can be used by other objects /// or threads to receive notice of cancellation.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <remarks>If the activities are successfully sent, the task result contains /// the items read, indexed by key.</remarks> /// <seealso cref="DeleteAsync(string[], CancellationToken)"/> /// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/> public async Task <IDictionary <string, object> > ReadAsync(string[] keys, CancellationToken cancellationToken) { FeedOptions options = null; if (keys == null) { throw new ArgumentNullException(nameof(keys)); } if (keys.Length == 0) { // No keys passed in, no result to return. return(new Dictionary <string, object>()); } // Ensure Initialization has been run await InitializeAsync().ConfigureAwait(false); if (!string.IsNullOrEmpty(this._partitionKey)) { options = new FeedOptions() { PartitionKey = new PartitionKey(this._partitionKey) }; } var storeItems = new Dictionary <string, object>(keys.Length); var parameterSequence = string.Join(",", Enumerable.Range(0, keys.Length).Select(i => $"@id{i}")); var parameterValues = keys.Select((key, ix) => new SqlParameter($"@id{ix}", CosmosDbKeyEscape.EscapeKey(key))); var querySpec = new SqlQuerySpec { QueryText = $"SELECT c.id, c.realId, c.document, c._etag FROM c WHERE c.id in ({parameterSequence})", Parameters = new SqlParameterCollection(parameterValues), }; var query = _client.CreateDocumentQuery <DocumentStoreItem>(_collectionLink, querySpec, options).AsDocumentQuery(); while (query.HasMoreResults) { foreach (var doc in await query.ExecuteNextAsync <DocumentStoreItem>(cancellationToken).ConfigureAwait(false)) { var item = doc.Document.ToObject(typeof(object), _jsonSerializer); if (item is IStoreItem storeItem) { storeItem.ETag = doc.ETag; } // doc.Id cannot be used since it is escaped, read it from RealId property instead storeItems.Add(doc.ReadlId, item); } } return(storeItems); }
public static string SanitizeKey(string key) => CosmosDbKeyEscape.EscapeKey(key);
/// <summary> /// Deletes storage items from storage. /// </summary> /// <param name="keys">keys of the <see cref="IStoreItem"/> objects to remove from the store.</param> /// <param name="cancellationToken">A cancellation token that can be used by other objects /// or threads to receive notice of cancellation.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <seealso cref="ReadAsync(string[], CancellationToken)"/> /// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/> public async Task DeleteAsync(string[] keys, CancellationToken cancellationToken) { if (keys == null || keys.Length == 0) { return; } // Ensure Initialization has been run await InitializeAsync().ConfigureAwait(false); // Parallelize deletion var tasks = keys.Select(key => _client.DeleteDocumentAsync( UriFactory.CreateDocumentUri(_databaseId, _collectionId, CosmosDbKeyEscape.EscapeKey(key)), cancellationToken: cancellationToken)); // await to deletion tasks to complete await Task.WhenAll(tasks).ConfigureAwait(false); }
/// <summary> /// Inserts or updates one or more items into the Cosmos DB container. /// </summary> /// <param name="changes">A dictionary of items to be inserted or updated. The dictionary item key /// is used as the ID for the inserted / updated item.</param> /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param> /// <returns>A Task representing the work to be executed.</returns> /// <exception cref="ArgumentNullException">Exception thrown if the changes dictionary is null.</exception> /// <exception cref="Exception">Exception thrown is the etag is empty on any of the items within the changes dictionary.</exception> public async Task WriteAsync(IDictionary <string, object> changes, CancellationToken cancellationToken = default(CancellationToken)) { if (changes == null) { throw new ArgumentNullException(nameof(changes)); } if (changes.Count == 0) { return; } // Ensure Initialization has been run await InitializeAsync().ConfigureAwait(false); foreach (var change in changes) { var json = JObject.FromObject(change.Value, _jsonSerializer); // Remove etag from JSON object that was copied from IStoreItem. // The ETag information is updated as an _etag attribute in the document metadata. json.Remove("eTag"); var documentChange = new DocumentStoreItem { Id = CosmosDbKeyEscape.EscapeKey(change.Key, _cosmosDbStorageOptions.KeySuffix, _cosmosDbStorageOptions.CompatibilityMode), RealId = change.Key, Document = json, }; var etag = (change.Value as IStoreItem)?.ETag; try { if (etag == null || etag == "*") { // if new item or * then insert or replace unconditionally await _container.UpsertItemAsync( documentChange, GetPartitionKey(documentChange.PartitionKey), cancellationToken : cancellationToken) .ConfigureAwait(false); } else if (etag.Length > 0) { // if we have an etag, do opt. concurrency replace await _container.UpsertItemAsync( documentChange, GetPartitionKey(documentChange.PartitionKey), new ItemRequestOptions() { IfMatchEtag = etag, }, cancellationToken) .ConfigureAwait(false); } else { throw new ArgumentException("etag empty"); } } catch (CosmosException ex) { // This check could potentially be performed before even attempting to upsert the item // so that a request wouldn't be made to Cosmos if it's expected to fail. // However, performing the check here ensures that this custom exception is only thrown // if Cosmos returns an error first. // This way, the nesting limit is not imposed on the Bot Framework side // and no exception will be thrown if the limit is eventually changed on the Cosmos side. if (IsNestingError(json, out var message)) { throw new InvalidOperationException(message, ex); } throw; } } }