From 9d1ac5eda217afce66c67e61c6b61ca5ecce8a8c Mon Sep 17 00:00:00 2001 From: DiscJockeyDJ <84394008+DiscJockeyDJ@users.noreply.github.com> Date: Wed, 15 Sep 2021 11:19:42 +0100 Subject: [PATCH] Stream transaction on Document API (#327) * Implement changes to Document API to include the stream transaction id to the request headers. fix #322 * Added test to verify the TransactionId HeadDocumentHeader value. Moved the TransactionId header string to a constant class. fix #322 * Add Trait attribute to the new tests added in DocumentApiClientTest to skip 3.4 ArangoDB test run. fix #322 --- .../CursorApi/CursorApiClientTest.cs | 5 +- .../DocumentApi/DocumentApiClientTest.cs | 84 +++++++-- .../UserApi/UserApiClientTest.cs | 12 +- .../CursorApi/CursorApiClient.cs | 2 +- arangodb-net-standard/CustomHttpHeaders.cs | 13 ++ .../DocumentApi/DocumentApiClient.cs | 178 ++++++++++++------ .../DocumentApi/IDocumentApiClient.cs | 82 +++++--- .../DocumentApi/Models/HeadDocumentHeader.cs | 12 ++ .../Transport/Http/HttpApiTransport.cs | 44 +++-- .../Transport/IApiClientTransport.cs | 21 ++- 10 files changed, 326 insertions(+), 127 deletions(-) create mode 100644 arangodb-net-standard/CustomHttpHeaders.cs diff --git a/arangodb-net-standard.Test/CursorApi/CursorApiClientTest.cs b/arangodb-net-standard.Test/CursorApi/CursorApiClientTest.cs index de874fd0..d3ea9837 100644 --- a/arangodb-net-standard.Test/CursorApi/CursorApiClientTest.cs +++ b/arangodb-net-standard.Test/CursorApi/CursorApiClientTest.cs @@ -246,7 +246,6 @@ public async Task PostCursorAsync_ShouldUseHeaderProperties() return Task.FromResult(mockResponse.Object); }); - string transactionHeaderKey = "x-arango-trx-id"; string dummyTransactionId = "dummy transaction Id"; // Call the method to create the cursor. @@ -264,8 +263,8 @@ await apiClient.PostCursorAsync( // Check that the header and values are there. Assert.NotNull(requestHeader); - Assert.Contains(transactionHeaderKey, requestHeader.AllKeys); - Assert.Equal(dummyTransactionId, requestHeader.Get(transactionHeaderKey)); + Assert.Contains(CustomHttpHeaders.StreamTransactionHeader, requestHeader.AllKeys); + Assert.Equal(dummyTransactionId, requestHeader.Get(CustomHttpHeaders.StreamTransactionHeader)); } [Fact] diff --git a/arangodb-net-standard.Test/DocumentApi/DocumentApiClientTest.cs b/arangodb-net-standard.Test/DocumentApi/DocumentApiClientTest.cs index 1e59a1a4..9e7fe3c3 100644 --- a/arangodb-net-standard.Test/DocumentApi/DocumentApiClientTest.cs +++ b/arangodb-net-standard.Test/DocumentApi/DocumentApiClientTest.cs @@ -1,15 +1,16 @@ -using ArangoDBNetStandard; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using ArangoDBNetStandard; using ArangoDBNetStandard.DocumentApi; using ArangoDBNetStandard.DocumentApi.Models; +using ArangoDBNetStandard.TransactionApi.Models; using ArangoDBNetStandard.Transport; using ArangoDBNetStandardTest.DocumentApi.Models; using Moq; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; using Xunit; namespace ArangoDBNetStandardTest.DocumentApi @@ -108,8 +109,8 @@ public async Task DeleteDocument_ShouldUseQueryParameters_WhenProvided() string requestUri = null; - mockTransport.Setup(x => x.DeleteAsync(It.IsAny())) - .Returns((string uri) => + mockTransport.Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny())) + .Returns((string uri, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); @@ -289,8 +290,8 @@ public async Task DeleteDocuments_ShouldUseQueryParameters_WhenProvided() string requestUri = null; - mockTransport.Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny())) - .Returns((string uri, byte[] content) => + mockTransport.Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string uri, byte[] content, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); @@ -619,8 +620,8 @@ public async Task PutDocument_ShouldUseQueryParameters_WhenProvided() string requestUri = null; - mockTransport.Setup(x => x.PutAsync(It.IsAny(), It.IsAny())) - .Returns((string uri, byte[] content) => + mockTransport.Setup(x => x.PutAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string uri, byte[] content, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); @@ -725,8 +726,8 @@ public async Task PutDocumentsAsync_ShouldUseQueryParameters_WhenProvided() string requestUri = null; - mockTransport.Setup(x => x.PutAsync(It.IsAny(), It.IsAny())) - .Returns((string uri, byte[] content) => + mockTransport.Setup(x => x.PutAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string uri, byte[] content, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); @@ -852,8 +853,8 @@ public async Task PatchDocumentsAsync_ShouldUseQueryParameters_WhenProvided() string requestUri = null; - mockTransport.Setup(x => x.PatchAsync(It.IsAny(), It.IsAny())) - .Returns((string uri, byte[] content) => + mockTransport.Setup(x => x.PatchAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string uri, byte[] content, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); @@ -1086,6 +1087,57 @@ public async Task ReadDocumentHeaderAsync_ShouldReturnPreconditionFailed_WhenIfM Assert.NotEqual($"\"{docResponse._rev}\"", response.Etag.Tag); } + [Fact] + [Trait("Feature", "StreamTransaction")] + public async Task ReadDocumentHeaderAsync_ShouldReturnOk_WhenTransactionIdIsGivenAndIsTheSame() + { + // Post a single document. + var docResponse = + await _docClient.PostDocumentAsync(_testCollection, new Dictionary { ["key"] = "value" }); + + // Begin a transaction. + var beginTransaction = await _adb.Transaction.BeginTransaction( + new StreamTransactionBody + { + Collections = new PostTransactionRequestCollections + { + Write = new[] { _testCollection } + } + }); + + // Get the header fields. + var response = await _docClient.HeadDocumentAsync( + _testCollection, + docResponse._key, + new HeadDocumentHeader { TransactionId = beginTransaction.Result.Id }); + + // Check for the expected status. + Assert.Equal(HttpStatusCode.OK, response.Code); + + // Abort the transaction. + await _adb.Transaction.AbortTransaction(beginTransaction.Result.Id); + } + + [Fact] + [Trait("Feature", "StreamTransaction")] + public async Task ReadDocumentHeaderAsync_ShouldReturnNotFound_WhenTransctionIdIsGiveAndIsNotTheSame() + { + string dummyTransactionId = "Bogus transaction Id"; + + // Post a single document. + var docResponse = + await _docClient.PostDocumentAsync(_testCollection, new Dictionary { ["key"] = "value" }); + + // Get the header fields. + var response = await _docClient.HeadDocumentAsync( + _testCollection, + docResponse._key, + new HeadDocumentHeader { TransactionId = dummyTransactionId }); + + // Check for the expected status. + Assert.Equal(HttpStatusCode.BadRequest, response.Code); + } + [Fact] public async Task ReadDocumentHeaderAsync_ShouldReturnNotFound_WhenCollectionDoesNotExist() { diff --git a/arangodb-net-standard.Test/UserApi/UserApiClientTest.cs b/arangodb-net-standard.Test/UserApi/UserApiClientTest.cs index 9f1c6d35..bbe2bc32 100644 --- a/arangodb-net-standard.Test/UserApi/UserApiClientTest.cs +++ b/arangodb-net-standard.Test/UserApi/UserApiClientTest.cs @@ -291,8 +291,8 @@ public async Task GetDatabaseAccessLevelAsync_ShouldThrow_WhenErrorResponseRetur string requestUri = null; - mockTransport.Setup(x => x.GetAsync(It.IsAny())) - .Returns((string uri) => + mockTransport.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) + .Returns((string uri, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); @@ -375,8 +375,8 @@ public async Task GetAccessibleDatabasesAsync_ShouldUseQueryParameters_WhenProvi string requestUri = null; - mockTransport.Setup(x => x.GetAsync(It.IsAny())) - .Returns((string uri) => + mockTransport.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) + .Returns((string uri, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); @@ -512,8 +512,8 @@ public async Task GetCollectionAccessLevelAsync_ShouldThrow_WhenErrorResponseRet string requestUri = null; - mockTransport.Setup(x => x.GetAsync(It.IsAny())) - .Returns((string uri) => + mockTransport.Setup(x => x.GetAsync(It.IsAny(), It.IsAny())) + .Returns((string uri, WebHeaderCollection webHeaderCollection) => { requestUri = uri; return Task.FromResult(mockResponse.Object); diff --git a/arangodb-net-standard/CursorApi/CursorApiClient.cs b/arangodb-net-standard/CursorApi/CursorApiClient.cs index 303d7353..39e0981c 100644 --- a/arangodb-net-standard/CursorApi/CursorApiClient.cs +++ b/arangodb-net-standard/CursorApi/CursorApiClient.cs @@ -57,7 +57,7 @@ protected virtual WebHeaderCollection GetHeaderCollection(CursorHeaderProperties { if (!string.IsNullOrWhiteSpace(headerProperties.TransactionId)) { - headerCollection.Add("x-arango-trx-id", headerProperties.TransactionId); + headerCollection.Add(CustomHttpHeaders.StreamTransactionHeader, headerProperties.TransactionId); } } diff --git a/arangodb-net-standard/CustomHttpHeaders.cs b/arangodb-net-standard/CustomHttpHeaders.cs new file mode 100644 index 00000000..9c087fb3 --- /dev/null +++ b/arangodb-net-standard/CustomHttpHeaders.cs @@ -0,0 +1,13 @@ +namespace ArangoDBNetStandard +{ + /// + /// The custom HttpHeaders that may be specified in a client request. + /// + public static class CustomHttpHeaders + { + /// + /// The header string used for Stream Transaction. + /// + public const string StreamTransactionHeader = "x-arango-trx-id"; + } +} diff --git a/arangodb-net-standard/DocumentApi/DocumentApiClient.cs b/arangodb-net-standard/DocumentApi/DocumentApiClient.cs index 63e3c24d..8a72c24a 100644 --- a/arangodb-net-standard/DocumentApi/DocumentApiClient.cs +++ b/arangodb-net-standard/DocumentApi/DocumentApiClient.cs @@ -1,9 +1,9 @@ -using ArangoDBNetStandard.DocumentApi.Models; -using ArangoDBNetStandard.Serialization; -using ArangoDBNetStandard.Transport; -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; using System.Threading.Tasks; +using ArangoDBNetStandard.DocumentApi.Models; +using ArangoDBNetStandard.Serialization; +using ArangoDBNetStandard.Transport; namespace ArangoDBNetStandard.DocumentApi { @@ -45,6 +45,17 @@ public DocumentApiClient(IApiClientTransport client, IApiClientSerialization ser _client = client; } + /// + /// Method to get the header collection. + /// + /// The values. + /// values. + protected virtual WebHeaderCollection GetHeaderCollection(HeadDocumentHeader headers) + { + var headerCollection = headers == null ? new WebHeaderCollection() : headers.ToWebHeaderCollection(); + return headerCollection; + } + /// /// Post a single document. /// @@ -54,18 +65,21 @@ public DocumentApiClient(IApiClientTransport client, IApiClientSerialization ser /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// public virtual Task> PostDocumentAsync( string collectionName, T document, PostDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null) + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null) { return PostDocumentAsync( collectionName, document, query, - serializationOptions); + serializationOptions, + headers); } /// @@ -81,26 +95,31 @@ public virtual Task> PostDocumentAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// public virtual async Task> PostDocumentAsync( string collectionName, T document, PostDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null) + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null) { string uriString = _docApiPath + "/" + WebUtility.UrlEncode(collectionName); if (query != null) { uriString += "?" + query.ToQueryString(); } + var content = GetContent(document, serializationOptions); - using (var response = await _client.PostAsync(uriString, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.PostAsync(uriString, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); return DeserializeJsonFromStream>(stream); } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -114,20 +133,24 @@ public virtual async Task> PostDocumentAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// public virtual async Task> PostDocumentsAsync( string collectionName, IList documents, PostDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null) + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null) { string uriString = _docApiPath + "/" + WebUtility.UrlEncode(collectionName); if (query != null) { uriString += "?" + query.ToQueryString(); } + var content = GetContent(documents, serializationOptions); - using (var response = await _client.PostAsync(uriString, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.PostAsync(uriString, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { @@ -141,6 +164,7 @@ public virtual async Task> PostDocumentsAsync( return DeserializeJsonFromStream>(stream); } } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -154,20 +178,24 @@ public virtual async Task> PostDocumentsAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// public virtual async Task> PutDocumentsAsync( string collectionName, IList documents, PutDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null) + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null) { string uri = _docApiPath + "/" + WebUtility.UrlEncode(collectionName); if (query != null) { uri += "?" + query.ToQueryString(); } + var content = GetContent(documents, serializationOptions); - using (var response = await _client.PutAsync(uri, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.PutAsync(uri, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { @@ -181,6 +209,7 @@ public virtual async Task> PutDocumentsAsync( return DeserializeJsonFromStream>(stream); } } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -193,14 +222,17 @@ public virtual async Task> PutDocumentsAsync( /// /// /// + /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// public virtual async Task> PutDocumentAsync( string documentId, T doc, PutDocumentQuery opts = null, - ApiClientSerializationOptions serializationOptions = null) + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null) { ValidateDocumentId(documentId); string uri = _docApiPath + "/" + documentId; @@ -208,14 +240,17 @@ public virtual async Task> PutDocumentAsync( { uri += "?" + opts.ToQueryString(); } + var content = GetContent(doc, serializationOptions); - using (var response = await _client.PutAsync(uri, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.PutAsync(uri, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); return DeserializeJsonFromStream>(stream); } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -230,17 +265,20 @@ public virtual async Task> PutDocumentAsync( /// /// /// + /// The values. /// public virtual Task> PutDocumentAsync( string collectionName, string documentKey, T doc, - PutDocumentQuery opts = null) + PutDocumentQuery opts = null, + HeadDocumentHeader headers = null) { return PutDocumentAsync( $"{WebUtility.UrlEncode(collectionName)}/{WebUtility.UrlEncode(documentKey)}", doc, - opts); + opts, + headers: headers); } /// @@ -249,11 +287,14 @@ public virtual Task> PutDocumentAsync( /// /// /// + /// The values. /// - public virtual async Task GetDocumentAsync(string collectionName, string documentKey) + public virtual async Task GetDocumentAsync( + string collectionName, string documentKey, HeadDocumentHeader headers = null) { return await GetDocumentAsync( - $"{WebUtility.UrlEncode(collectionName)}/{WebUtility.UrlEncode(documentKey)}").ConfigureAwait(false); + $"{WebUtility.UrlEncode(collectionName)}/{WebUtility.UrlEncode(documentKey)}", headers) + .ConfigureAwait(false); } /// @@ -261,17 +302,20 @@ public virtual async Task GetDocumentAsync(string collectionName, string d /// /// /// + /// The values. /// - public virtual async Task GetDocumentAsync(string documentId) + public virtual async Task GetDocumentAsync(string documentId, HeadDocumentHeader headers = null) { ValidateDocumentId(documentId); - var response = await _client.GetAsync(_docApiPath + "/" + documentId).ConfigureAwait(false); + var headerCollection = GetHeaderCollection(headers); + var response = await _client.GetAsync(_docApiPath + "/" + documentId, headerCollection).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var document = DeserializeJsonFromStream(stream); return document; } + throw await GetApiErrorException(response).ConfigureAwait(false); } @@ -282,16 +326,17 @@ public virtual async Task GetDocumentAsync(string documentId) /// deserialized from the response. /// Collection name /// Document keys to fetch documents for + /// The values. /// public virtual async Task> GetDocumentsAsync( string collectionName, - IList selectors) + IList selectors, + HeadDocumentHeader headers = null) { string uri = $"{_docApiPath}/{WebUtility.UrlEncode(collectionName)}?onlyget=true"; - var content = GetContent(selectors, new ApiClientSerializationOptions(false, true)); - - using (var response = await _client.PutAsync(uri, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.PutAsync(uri, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { @@ -299,6 +344,7 @@ public virtual async Task> GetDocumentsAsync( var documents = DeserializeJsonFromStream>(stream); return documents; } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -315,15 +361,17 @@ public virtual async Task> GetDocumentsAsync( /// /// /// + /// The values. /// public virtual async Task> DeleteDocumentAsync( string collectionName, string documentKey, - DeleteDocumentQuery query = null) + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null) { return await DeleteDocumentAsync( $"{WebUtility.UrlEncode(collectionName)}/{WebUtility.UrlEncode(documentKey)}", - query).ConfigureAwait(false); + query, headers).ConfigureAwait(false); } /// @@ -337,12 +385,14 @@ public virtual async Task> DeleteDocumentAsync( /// /// /// + /// The values. /// public virtual async Task> DeleteDocumentAsync( string documentId, - DeleteDocumentQuery query = null) + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null) { - return await DeleteDocumentAsync(documentId, query).ConfigureAwait(false); + return await DeleteDocumentAsync(documentId, query, headers).ConfigureAwait(false); } /// @@ -352,15 +402,17 @@ public virtual async Task> DeleteDocumentAsync( /// /// /// + /// The values. /// public virtual async Task> DeleteDocumentAsync( string collectionName, string documentKey, - DeleteDocumentQuery query = null) + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null) { return await DeleteDocumentAsync( $"{WebUtility.UrlEncode(collectionName)}/{WebUtility.UrlEncode(documentKey)}", - query).ConfigureAwait(false); + query, headers).ConfigureAwait(false); } /// @@ -368,10 +420,12 @@ public virtual async Task> DeleteDocumentAsync( /// /// /// + /// The values. /// public virtual async Task> DeleteDocumentAsync( string documentId, - DeleteDocumentQuery query = null) + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null) { ValidateDocumentId(documentId); string uri = _docApiPath + "/" + documentId; @@ -379,7 +433,9 @@ public virtual async Task> DeleteDocumentAsync( { uri += "?" + query.ToQueryString(); } - using (var response = await _client.DeleteAsync(uri).ConfigureAwait(false)) + + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.DeleteAsync(uri, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { @@ -387,6 +443,7 @@ public virtual async Task> DeleteDocumentAsync( var responseModel = DeserializeJsonFromStream>(stream); return responseModel; } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -404,13 +461,15 @@ public virtual async Task> DeleteDocumentAsync( /// /// /// + /// The values. /// public virtual async Task> DeleteDocumentsAsync( string collectionName, IList selectors, - DeleteDocumentsQuery query = null) + DeleteDocumentsQuery query = null, + HeadDocumentHeader headers = null) { - return await DeleteDocumentsAsync(collectionName, selectors, query).ConfigureAwait(false); + return await DeleteDocumentsAsync(collectionName, selectors, query, headers).ConfigureAwait(false); } /// @@ -421,19 +480,23 @@ public virtual async Task> DeleteDocumentsAsync( /// /// /// + /// The values. /// public virtual async Task> DeleteDocumentsAsync( string collectionName, IList selectors, - DeleteDocumentsQuery query = null) + DeleteDocumentsQuery query = null, + HeadDocumentHeader headers = null) { string uri = _docApiPath + "/" + WebUtility.UrlEncode(collectionName); if (query != null) { uri += "?" + query.ToQueryString(); } + var content = GetContent(selectors, new ApiClientSerializationOptions(false, false)); - using (var response = await _client.DeleteAsync(uri, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.DeleteAsync(uri, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { @@ -447,6 +510,7 @@ public virtual async Task> DeleteDocumentsAsync( return DeserializeJsonFromStream>(stream); } } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -474,20 +538,24 @@ public virtual async Task> DeleteDocumentsAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// public virtual async Task> PatchDocumentsAsync( string collectionName, IList patches, PatchDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null) + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null) { string uri = _docApiPath + "/" + WebUtility.UrlEncode(collectionName); if (query != null) { uri += "?" + query.ToQueryString(); } + var content = GetContent(patches, serializationOptions); - using (var response = await _client.PatchAsync(uri, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.PatchAsync(uri, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { @@ -501,6 +569,7 @@ public virtual async Task> PatchDocumentsAsync( return DeserializeJsonFromStream>(stream); } } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -522,17 +591,19 @@ public virtual async Task> PatchDocumentsAsync( /// /// /// + /// The values. /// public virtual async Task> PatchDocumentAsync( string collectionName, string documentKey, T body, - PatchDocumentQuery query = null) + PatchDocumentQuery query = null, + HeadDocumentHeader headers = null) { string documentHandle = WebUtility.UrlEncode(collectionName) + "/" + WebUtility.UrlEncode(documentKey); - return await PatchDocumentAsync(documentHandle, body, query).ConfigureAwait(false); + return await PatchDocumentAsync(documentHandle, body, query, headers: headers).ConfigureAwait(false); } /// @@ -553,12 +624,14 @@ public virtual async Task> PatchDocumentAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// public virtual async Task> PatchDocumentAsync( string documentId, T body, PatchDocumentQuery query = null, - ApiClientSerializationOptions serializationOptions = null) + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null) { ValidateDocumentId(documentId); string uriString = _docApiPath + "/" + documentId; @@ -566,14 +639,17 @@ public virtual async Task> PatchDocumentAsync( { uriString += "?" + query.ToQueryString(); } + var content = GetContent(body, serializationOptions); - using (var response = await _client.PatchAsync(uriString, content).ConfigureAwait(false)) + var headerCollection = GetHeaderCollection(headers); + using (var response = await _client.PatchAsync(uriString, content, headerCollection).ConfigureAwait(false)) { if (response.IsSuccessStatusCode) { var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); return DeserializeJsonFromStream>(stream); } + throw await GetApiErrorException(response).ConfigureAwait(false); } } @@ -586,10 +662,11 @@ public virtual async Task> PatchDocumentAsync( /// /// /// - /// + /// The values. /// /// 200: is returned if the document was found. /// 304: is returned if the “If-None-Match” header is given and the document has the same version. + /// 400: is returned if the "TransactionId" header is given and the transactionId does not exist. /// 404: is returned if the document or collection was not found. /// 412: is returned if an “If-Match” header is given and the found document has a different version. The response will also contain the found document’s current revision in the Etag header. /// @@ -611,11 +688,12 @@ public virtual async Task HeadDocumentAsync( /// HEAD/_api/document/{document-handle} /// /// - /// Object containing a dictionary of Header keys and values + /// The values. /// Document ID is invalid. /// /// 200: is returned if the document was found. /// 304: is returned if the “If-None-Match” header is given and the document has the same version. + /// 400: is returned if the "TransactionId" header is given and the transactionId does not exist. /// 404: is returned if the document or collection was not found. /// 412: is returned if an “If-Match” header is given and the found document has a different version. The response will also contain the found document’s current revision in the Etag header. /// @@ -626,16 +704,8 @@ public virtual async Task HeadDocumentAsync( { ValidateDocumentId(documentId); string uri = _docApiPath + "/" + documentId; - WebHeaderCollection headerCollection; - if (headers == null) - { - headerCollection = new WebHeaderCollection(); - } - else - { - headerCollection = headers.ToWebHeaderCollection(); - } - using (var response = await _client.HeadAsync(uri, headerCollection).ConfigureAwait(false)) + WebHeaderCollection headerCollection = GetHeaderCollection(headers); + using (var response = await _client.HeadAsync(uri, headerCollection)) { return new HeadDocumentResponse { diff --git a/arangodb-net-standard/DocumentApi/IDocumentApiClient.cs b/arangodb-net-standard/DocumentApi/IDocumentApiClient.cs index 559de4df..03720dda 100644 --- a/arangodb-net-standard/DocumentApi/IDocumentApiClient.cs +++ b/arangodb-net-standard/DocumentApi/IDocumentApiClient.cs @@ -1,8 +1,8 @@ -using ArangoDBNetStandard.DocumentApi.Models; -using ArangoDBNetStandard.Serialization; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using ArangoDBNetStandard.DocumentApi.Models; +using ArangoDBNetStandard.Serialization; namespace ArangoDBNetStandard.DocumentApi { @@ -20,12 +20,14 @@ public interface IDocumentApiClient /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// Task> PostDocumentAsync( string collectionName, T document, PostDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null); + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null); /// /// Post a single document with the possibility to specify a different type @@ -40,12 +42,14 @@ Task> PostDocumentAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// Task> PostDocumentAsync( string collectionName, T document, PostDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null); + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null); /// /// Post multiple documents in a single request. @@ -56,12 +60,14 @@ Task> PostDocumentAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// Task> PostDocumentsAsync( string collectionName, IList documents, PostDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null); + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null); /// /// Replace multiple documents. @@ -72,12 +78,14 @@ Task> PostDocumentsAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// Task> PutDocumentsAsync( string collectionName, IList documents, PutDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null); + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null); /// /// Replaces the document with the provided document ID with the one in @@ -90,12 +98,14 @@ Task> PutDocumentsAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// Task> PutDocumentAsync( string documentId, T doc, PutDocumentQuery opts = null, - ApiClientSerializationOptions serializationOptions = null); + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null); /// /// Replaces the document based on its Document ID with the one in @@ -107,12 +117,14 @@ Task> PutDocumentAsync( /// /// /// + /// The values. /// Task> PutDocumentAsync( string collectionName, string documentKey, T doc, - PutDocumentQuery opts = null); + PutDocumentQuery opts = null, + HeadDocumentHeader headers = null); /// /// Get an existing document. @@ -120,16 +132,18 @@ Task> PutDocumentAsync( /// /// /// + /// The values. /// - Task GetDocumentAsync(string collectionName, string documentKey); + Task GetDocumentAsync(string collectionName, string documentKey, HeadDocumentHeader headers = null); /// /// Get an existing document based on its Document ID. /// /// /// + /// The values. /// - Task GetDocumentAsync(string documentId); + Task GetDocumentAsync(string documentId, HeadDocumentHeader headers = null); /// /// Get multiple documents. @@ -138,10 +152,12 @@ Task> PutDocumentAsync( /// deserialized from the response. /// Collection name /// Document keys to fetch documents for + /// The values. /// Task> GetDocumentsAsync( string collectionName, - IList selectors); + IList selectors, + HeadDocumentHeader headers = null); /// /// Delete a document. @@ -155,11 +171,13 @@ Task> GetDocumentsAsync( /// /// /// + /// The values. /// Task> DeleteDocumentAsync( string collectionName, string documentKey, - DeleteDocumentQuery query = null); + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null); /// /// Delete a document based on its document ID. @@ -172,10 +190,12 @@ Task> DeleteDocumentAsync( /// /// /// + /// The values. /// Task> DeleteDocumentAsync( string documentId, - DeleteDocumentQuery query = null); + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null); /// /// Delete a document. @@ -184,21 +204,25 @@ Task> DeleteDocumentAsync( /// /// /// + /// The values. /// Task> DeleteDocumentAsync( string collectionName, string documentKey, - DeleteDocumentQuery query = null); + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null); /// /// Delete a document based on its document ID. /// /// /// + /// The values. /// Task> DeleteDocumentAsync( string documentId, - DeleteDocumentQuery query = null); + DeleteDocumentQuery query = null, + HeadDocumentHeader headers = null); /// /// Delete multiple documents based on the passed document selectors. @@ -213,11 +237,13 @@ Task> DeleteDocumentAsync( /// /// /// + /// The values. /// Task> DeleteDocumentsAsync( string collectionName, IList selectors, - DeleteDocumentsQuery query = null); + DeleteDocumentsQuery query = null, + HeadDocumentHeader headers = null); /// /// Delete multiple documents based on the passed document selectors. @@ -227,11 +253,13 @@ Task> DeleteDocumentsAsync( /// /// /// + /// The values. /// Task> DeleteDocumentsAsync( string collectionName, IList selectors, - DeleteDocumentsQuery query = null); + DeleteDocumentsQuery query = null, + HeadDocumentHeader headers = null); /// /// Partially updates documents, the documents to update are specified @@ -258,12 +286,14 @@ Task> DeleteDocumentsAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// Task> PatchDocumentsAsync( string collectionName, IList patches, PatchDocumentsQuery query = null, - ApiClientSerializationOptions serializationOptions = null); + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null); /// /// Partially updates the document identified by document-handle. @@ -282,12 +312,14 @@ Task> PatchDocumentsAsync( /// /// /// + /// The values. /// Task> PatchDocumentAsync( string collectionName, string documentKey, T body, - PatchDocumentQuery query = null); + PatchDocumentQuery query = null, + HeadDocumentHeader headers = null); /// /// Partially updates the document identified by document-handle. @@ -307,12 +339,14 @@ Task> PatchDocumentAsync( /// /// The serialization options. When the value is null the /// the serialization options should be provided by the serializer, otherwise the given options should be used. + /// The values. /// Task> PatchDocumentAsync( string documentId, T body, PatchDocumentQuery query = null, - ApiClientSerializationOptions serializationOptions = null); + ApiClientSerializationOptions serializationOptions = null, + HeadDocumentHeader headers = null); /// /// Like GET, but only returns the header fields and not the body. You @@ -322,10 +356,11 @@ Task> PatchDocumentAsync( /// /// /// - /// + /// The values. /// /// 200: is returned if the document was found. /// 304: is returned if the “If-None-Match” header is given and the document has the same version. + /// 400: is returned if the "TransactionId" header is given and the transactionId does not exist. /// 404: is returned if the document or collection was not found. /// 412: is returned if an “If-Match” header is given and the found document has a different version. The response will also contain the found document’s current revision in the Etag header. /// @@ -342,11 +377,12 @@ Task HeadDocumentAsync( /// HEAD/_api/document/{document-handle} /// /// - /// Object containing a dictionary of Header keys and values + /// The values. /// Document ID is invalid. /// /// 200: is returned if the document was found. /// 304: is returned if the “If-None-Match” header is given and the document has the same version. + /// 400: is returned if the "TransactionId" header is given and the transactionId does not exist. /// 404: is returned if the document or collection was not found. /// 412: is returned if an “If-Match” header is given and the found document has a different version. The response will also contain the found document’s current revision in the Etag header. /// diff --git a/arangodb-net-standard/DocumentApi/Models/HeadDocumentHeader.cs b/arangodb-net-standard/DocumentApi/Models/HeadDocumentHeader.cs index d33c69a9..dbb1d6d5 100644 --- a/arangodb-net-standard/DocumentApi/Models/HeadDocumentHeader.cs +++ b/arangodb-net-standard/DocumentApi/Models/HeadDocumentHeader.cs @@ -8,6 +8,11 @@ public class HeadDocumentHeader public string IfNoneMatch { get; set; } + /// + /// Gets or sets the Id of a stream transaction. + /// + public string TransactionId { get; set; } + public WebHeaderCollection ToWebHeaderCollection() { WebHeaderCollection collection = new WebHeaderCollection(); @@ -15,10 +20,17 @@ public WebHeaderCollection ToWebHeaderCollection() { collection.Add(HttpRequestHeader.IfMatch, $"\"{IfMatch}\""); } + if (IfNoneMatch != null) { collection.Add(HttpRequestHeader.IfNoneMatch, $"\"{IfNoneMatch}\""); } + + if (TransactionId != null) + { + collection.Add(CustomHttpHeaders.StreamTransactionHeader, TransactionId); + } + return collection; } } diff --git a/arangodb-net-standard/Transport/Http/HttpApiTransport.cs b/arangodb-net-standard/Transport/Http/HttpApiTransport.cs index c0dbbb9c..37cf9c0d 100644 --- a/arangodb-net-standard/Transport/Http/HttpApiTransport.cs +++ b/arangodb-net-standard/Transport/Http/HttpApiTransport.cs @@ -184,10 +184,14 @@ public void SetJwtToken(string jwt) /// Sends a DELETE request using . /// /// + /// Object containing a dictionary of Header keys and values. /// - public async Task DeleteAsync(string requestUri) + public async Task DeleteAsync( + string requestUri, WebHeaderCollection webHeaderCollection = null) { - var response = await _client.DeleteAsync(requestUri).ConfigureAwait(false); + var request = new HttpRequestMessage(HttpMethod.Delete, requestUri); + ApplyHeaders(webHeaderCollection, request.Headers); + var response = await _client.SendAsync(request).ConfigureAwait(false); return new HttpApiClientResponse(response); } @@ -196,14 +200,18 @@ public async Task DeleteAsync(string requestUri) /// /// /// + /// Object containing a dictionary of Header keys and values. /// - public async Task DeleteAsync(string requestUri, byte[] content) + public async Task DeleteAsync( + string requestUri, byte[] content, WebHeaderCollection webHeaderCollection = null) { var request = new HttpRequestMessage(HttpMethod.Delete, requestUri) { Content = new ByteArrayContent(content) }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue(_contentTypeMap[_contentType]); + ApplyHeaders(webHeaderCollection, request.Content.Headers); var response = await _client.SendAsync(request).ConfigureAwait(false); return new HttpApiClientResponse(response); } @@ -230,12 +238,14 @@ public async Task PostAsync( /// /// /// The content of the request, must not be null. + /// Object containing a dictionary of Header keys and values. /// public async Task PutAsync( - string requestUri, byte[] content) + string requestUri, byte[] content, WebHeaderCollection webHeaderCollection = null) { var httpContent = new ByteArrayContent(content); httpContent.Headers.ContentType = new MediaTypeHeaderValue(_contentTypeMap[_contentType]); + ApplyHeaders(webHeaderCollection, httpContent.Headers); var response = await _client.PutAsync(requestUri, httpContent).ConfigureAwait(false); return new HttpApiClientResponse(response); } @@ -244,10 +254,13 @@ public async Task PutAsync( /// Sends a GET request using . /// /// The content of the request, must not be null. + /// Object containing a dictionary of Header keys and values. /// - public async Task GetAsync(string requestUri) + public async Task GetAsync(string requestUri, WebHeaderCollection webHeaderCollection = null) { - var response = await _client.GetAsync(requestUri).ConfigureAwait(false); + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + ApplyHeaders(webHeaderCollection, request.Headers); + var response = await _client.SendAsync(request).ConfigureAwait(false); return new HttpApiClientResponse(response); } @@ -256,15 +269,19 @@ public async Task GetAsync(string requestUri) /// /// /// The content of the request, must not be null. + /// Object containing a dictionary of Header keys and values. /// - public async Task PatchAsync(string requestUri, byte[] content) + public async Task PatchAsync( + string requestUri, byte[] content, WebHeaderCollection webHeaderCollection = null) { var method = new HttpMethod("PATCH"); var request = new HttpRequestMessage(method, requestUri) { Content = new ByteArrayContent(content) }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue(_contentTypeMap[_contentType]); + ApplyHeaders(webHeaderCollection, request.Content.Headers); var response = await _client.SendAsync(request).ConfigureAwait(false); return new HttpApiClientResponse(response); } @@ -273,22 +290,15 @@ public async Task PatchAsync(string requestUri, byte[] conte /// Send a HEAD request using /// /// - /// + /// Object containing a dictionary of Header keys and values. /// public async Task HeadAsync( string requestUri, WebHeaderCollection webHeaderCollection = null) { var request = new HttpRequestMessage(HttpMethod.Head, requestUri); - - if (webHeaderCollection != null) - { - foreach (var key in webHeaderCollection.AllKeys) - { - request.Headers.Add(key, webHeaderCollection[key]); - } - } - var response = await _client.SendAsync(request).ConfigureAwait(false); + ApplyHeaders(webHeaderCollection, request.Headers); + var response = await _client.SendAsync(request); return new HttpApiClientResponse(response); } diff --git a/arangodb-net-standard/Transport/IApiClientTransport.cs b/arangodb-net-standard/Transport/IApiClientTransport.cs index 480af710..096bf0cc 100644 --- a/arangodb-net-standard/Transport/IApiClientTransport.cs +++ b/arangodb-net-standard/Transport/IApiClientTransport.cs @@ -23,47 +23,54 @@ Task PostAsync( /// Send a DELETE request. /// /// + /// Object containing a dictionary of Header keys and values. /// - Task DeleteAsync(string requestUri); + Task DeleteAsync(string requestUri, WebHeaderCollection webHeaderCollection = null); /// /// Send a DELETE request with body content. /// /// /// + /// Object containing a dictionary of Header keys and values. /// - Task DeleteAsync(string requestUri, byte[] content); + Task DeleteAsync( + string requestUri, byte[] content, WebHeaderCollection webHeaderCollection = null); /// /// Send a PUT request. /// /// /// + /// Object containing a dictionary of Header keys and values. /// Task PutAsync( - string requestUri, byte[] content); + string requestUri, byte[] content, WebHeaderCollection webHeaderCollection = null); /// /// Send a GET request. /// /// + /// Object containing a dictionary of Header keys and values. /// - Task GetAsync(string requestUri); + Task GetAsync(string requestUri, WebHeaderCollection webHeaderCollection = null); /// /// Send a PATCH request. /// /// /// + /// Object containing a dictionary of Header keys and values. /// - Task PatchAsync(string requestUri, byte[] content); + Task PatchAsync( + string requestUri, byte[] content, WebHeaderCollection webHeaderCollection = null); /// /// Send a HEAD Request. /// /// - /// + /// Object containing a dictionary of Header keys and values. /// - Task HeadAsync(string requestUri, WebHeaderCollection httpRequestHeaders); + Task HeadAsync(string requestUri, WebHeaderCollection webHeaderCollection = null); } }