From 41c3e8a73d91ce862166f50a740bc267dd0f3767 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 24 Aug 2024 16:22:02 +0200 Subject: [PATCH 01/28] Improve DDB error handling efficiency --- .../EfficientDynamoDb.csproj | 1 + .../Internal/ErrorHandler.cs | 10 +-- src/EfficientDynamoDb/Internal/HttpApi.cs | 76 ++++++------------- 3 files changed, 31 insertions(+), 56 deletions(-) diff --git a/src/EfficientDynamoDb/EfficientDynamoDb.csproj b/src/EfficientDynamoDb/EfficientDynamoDb.csproj index 43f96361..ba49858f 100644 --- a/src/EfficientDynamoDb/EfficientDynamoDb.csproj +++ b/src/EfficientDynamoDb/EfficientDynamoDb.csproj @@ -15,6 +15,7 @@ true true snupkg + 9 diff --git a/src/EfficientDynamoDb/Internal/ErrorHandler.cs b/src/EfficientDynamoDb/Internal/ErrorHandler.cs index e4c577d0..9cd30ec4 100644 --- a/src/EfficientDynamoDb/Internal/ErrorHandler.cs +++ b/src/EfficientDynamoDb/Internal/ErrorHandler.cs @@ -22,13 +22,13 @@ internal static class ErrorHandler PropertyNameCaseInsensitive = true, }; - public static async Task ProcessErrorAsync(DynamoDbContextMetadata metadata, HttpResponseMessage response, CancellationToken cancellationToken = default) + public static async Task ProcessErrorAsync(DynamoDbContextMetadata metadata, HttpResponseMessage response, CancellationToken cancellationToken = default) { try { await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.ServiceUnavailable) - throw new ServiceUnavailableException("DynamoDB is currently unavailable. (This should be a temporary state.)"); + return new ServiceUnavailableException("DynamoDB is currently unavailable. (This should be a temporary state.)"); var recyclableStream = new RecyclableMemoryStream(DynamoDbHttpContent.MemoryStreamManager); try @@ -42,11 +42,11 @@ public static async Task ProcessErrorAsync(DynamoDbContextMetadata metadata, Htt switch (response.StatusCode) { case HttpStatusCode.BadRequest: - throw await ProcessBadRequestAsync(metadata, recyclableStream, error, cancellationToken); + return await ProcessBadRequestAsync(metadata, recyclableStream, error, cancellationToken).ConfigureAwait(false); case HttpStatusCode.InternalServerError: - throw new InternalServerErrorException(error.Message); + return new InternalServerErrorException(error.Message); default: - throw new DdbException(error.Message); + return new DdbException(error.Message); } } finally diff --git a/src/EfficientDynamoDb/Internal/HttpApi.cs b/src/EfficientDynamoDb/Internal/HttpApi.cs index 58211a54..81fe3acf 100644 --- a/src/EfficientDynamoDb/Internal/HttpApi.cs +++ b/src/EfficientDynamoDb/Internal/HttpApi.cs @@ -32,68 +32,42 @@ public async ValueTask SendAsync(DynamoDbContextConfig conf throttlingRetries = 0; while (true) { - TimeSpan delay; + using var request = new HttpRequestMessage(HttpMethod.Post, config.RegionEndpoint.RequestUri); + request.Content = httpContent; + try { - using var request = new HttpRequestMessage(HttpMethod.Post, config.RegionEndpoint.RequestUri) - { - Content = httpContent - }; - - try - { - var httpClient = _httpClientFactory.CreateHttpClient(); - var stream = await httpContent.ReadAsStreamAsync().ConfigureAwait(false); - var credentials = await config.CredentialsProvider.GetCredentialsAsync(cancellationToken).ConfigureAwait(false); + var httpClient = _httpClientFactory.CreateHttpClient(); + var stream = await httpContent.ReadAsStreamAsync().ConfigureAwait(false); + var credentials = await config.CredentialsProvider.GetCredentialsAsync(cancellationToken).ConfigureAwait(false); - var metadata = new SigningMetadata(config.RegionEndpoint, credentials, DateTime.UtcNow, httpClient.DefaultRequestHeaders, - httpClient.BaseAddress); - AwsRequestSigner.Sign(request, (RecyclableMemoryStream) stream, in metadata); - - var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var metadata = new SigningMetadata(config.RegionEndpoint, credentials, DateTime.UtcNow, httpClient.DefaultRequestHeaders, httpClient.BaseAddress); + AwsRequestSigner.Sign(request, (RecyclableMemoryStream) stream, in metadata); - if (!response.IsSuccessStatusCode) - await ErrorHandler.ProcessErrorAsync(config.Metadata, response, cancellationToken).ConfigureAwait(false); + var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + if (response.IsSuccessStatusCode) return response; - } - finally + + var error = await ErrorHandler.ProcessErrorAsync(config.Metadata, response, cancellationToken).ConfigureAwait(false); + switch (error) { - request.Content = null; + case ProvisionedThroughputExceededException when config.RetryStrategies.ProvisionedThroughputExceededStrategy.TryGetRetryDelay(provisionedThroughputExceededRetries++, out var delay): + case LimitExceededException when config.RetryStrategies.LimitExceededStrategy.TryGetRetryDelay(limitExceededRetries++, out delay): + case InternalServerErrorException when config.RetryStrategies.InternalServerErrorStrategy.TryGetRetryDelay(internalServerErrorRetries++, out delay): + case RequestLimitExceededException when config.RetryStrategies.RequestLimitExceededStrategy.TryGetRetryDelay(requestLimitExceededRetries++, out delay): + case ServiceUnavailableException when config.RetryStrategies.ServiceUnavailableStrategy.TryGetRetryDelay(serviceUnavailableRetries++, out delay): + case ThrottlingException when config.RetryStrategies.ThrottlingStrategy.TryGetRetryDelay(throttlingRetries++, out delay): + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + break; + case not null: + throw error; } } - catch (InternalServerErrorException) - { - if (!config.RetryStrategies.InternalServerErrorStrategy.TryGetRetryDelay(internalServerErrorRetries++, out delay)) - throw; - } - catch (LimitExceededException) + finally { - if (!config.RetryStrategies.LimitExceededStrategy.TryGetRetryDelay(limitExceededRetries++, out delay)) - throw; + request.Content = null; } - catch (ProvisionedThroughputExceededException) - { - if (!config.RetryStrategies.ProvisionedThroughputExceededStrategy.TryGetRetryDelay(provisionedThroughputExceededRetries++, out delay)) - throw; - } - catch (RequestLimitExceededException) - { - if (!config.RetryStrategies.RequestLimitExceededStrategy.TryGetRetryDelay(requestLimitExceededRetries++, out delay)) - throw; - } - catch (ServiceUnavailableException) - { - if (!config.RetryStrategies.ServiceUnavailableStrategy.TryGetRetryDelay(serviceUnavailableRetries++, out delay)) - throw; - } - catch (ThrottlingException) - { - if (!config.RetryStrategies.ThrottlingStrategy.TryGetRetryDelay(throttlingRetries++, out delay)) - throw; - } - - await Task.Delay(delay, cancellationToken).ConfigureAwait(false); } } finally From d9c6612e194f71b110e6d43bea074347babc6263 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sun, 8 Sep 2024 17:59:02 +0200 Subject: [PATCH 02/28] WIP: OpResult --- .../Exceptions/AccessDeniedException.cs | 6 +- .../Exceptions/ChecksumMismatchException.cs | 2 - .../ConditionalCheckFailedException.cs | 7 +- .../Exceptions/DdbException.cs | 4 +- .../IdempotentParameterMismatchException.cs | 6 +- .../IncompleteSignatureException.cs | 6 +- .../InternalServerErrorException.cs | 6 +- ...temCollectionSizeLimitExceededException.cs | 6 +- .../Exceptions/LimitExceededException.cs | 6 +- .../MissingAuthenticationTokenException.cs | 6 +- .../ProvisionedThroughputExceededException.cs | 6 +- .../RequestLimitExceededException.cs | 6 +- .../Exceptions/ResourceInUseException.cs | 6 +- .../Exceptions/ResourceNotFoundException.cs | 6 +- .../Exceptions/RetryableException.cs | 1 - .../Exceptions/ServiceUnavailableException.cs | 6 +- .../Exceptions/ThrottlingException.cs | 6 +- .../TransactionCanceledException.cs | 5 + .../TransactionInProgressException.cs | 6 +- .../Exceptions/UnrecognizedClientException.cs | 6 +- .../Exceptions/ValidationException.cs | 6 +- src/EfficientDynamoDb/Operations/OpResult.cs | 202 ++++++++++++++++++ 22 files changed, 295 insertions(+), 22 deletions(-) create mode 100644 src/EfficientDynamoDb/Operations/OpResult.cs diff --git a/src/EfficientDynamoDb/Exceptions/AccessDeniedException.cs b/src/EfficientDynamoDb/Exceptions/AccessDeniedException.cs index 25adb0ad..c37dc168 100644 --- a/src/EfficientDynamoDb/Exceptions/AccessDeniedException.cs +++ b/src/EfficientDynamoDb/Exceptions/AccessDeniedException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class AccessDeniedException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.AccessDenied; + public AccessDeniedException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/ChecksumMismatchException.cs b/src/EfficientDynamoDb/Exceptions/ChecksumMismatchException.cs index ae800400..bb5b9cd8 100644 --- a/src/EfficientDynamoDb/Exceptions/ChecksumMismatchException.cs +++ b/src/EfficientDynamoDb/Exceptions/ChecksumMismatchException.cs @@ -1,5 +1,3 @@ -using System.Runtime.Serialization; - namespace EfficientDynamoDb.Exceptions { /// diff --git a/src/EfficientDynamoDb/Exceptions/ConditionalCheckFailedException.cs b/src/EfficientDynamoDb/Exceptions/ConditionalCheckFailedException.cs index 5e114638..275fb607 100644 --- a/src/EfficientDynamoDb/Exceptions/ConditionalCheckFailedException.cs +++ b/src/EfficientDynamoDb/Exceptions/ConditionalCheckFailedException.cs @@ -1,7 +1,6 @@ using System; -using System.Runtime.Serialization; using EfficientDynamoDb.DocumentModel; -using EfficientDynamoDb.Operations.Shared; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -12,6 +11,10 @@ namespace EfficientDynamoDb.Exceptions public class ConditionalCheckFailedException : DdbException { public Document? Item { get; } + + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.ConditionalCheckFailed; public ConditionalCheckFailedException(Document? item, string message) : base(message) { diff --git a/src/EfficientDynamoDb/Exceptions/DdbException.cs b/src/EfficientDynamoDb/Exceptions/DdbException.cs index 6c27ca58..5864bb6f 100644 --- a/src/EfficientDynamoDb/Exceptions/DdbException.cs +++ b/src/EfficientDynamoDb/Exceptions/DdbException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -15,5 +15,7 @@ public DdbException(string message) : base(message) public DdbException(string message, Exception innerException) : base(message, innerException) { } + + internal virtual OpErrorType OpErrorType => OpErrorType.Unknown; } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Exceptions/IdempotentParameterMismatchException.cs b/src/EfficientDynamoDb/Exceptions/IdempotentParameterMismatchException.cs index 0528fbaa..173dfa1e 100644 --- a/src/EfficientDynamoDb/Exceptions/IdempotentParameterMismatchException.cs +++ b/src/EfficientDynamoDb/Exceptions/IdempotentParameterMismatchException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -8,6 +8,10 @@ namespace EfficientDynamoDb.Exceptions /// public class IdempotentParameterMismatchException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.IdempotentParameterMismatch; + public IdempotentParameterMismatchException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/IncompleteSignatureException.cs b/src/EfficientDynamoDb/Exceptions/IncompleteSignatureException.cs index b81121d9..57b38163 100644 --- a/src/EfficientDynamoDb/Exceptions/IncompleteSignatureException.cs +++ b/src/EfficientDynamoDb/Exceptions/IncompleteSignatureException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class IncompleteSignatureException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.IncompleteSignature; + public IncompleteSignatureException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/InternalServerErrorException.cs b/src/EfficientDynamoDb/Exceptions/InternalServerErrorException.cs index 77ebbfd8..48748dc0 100644 --- a/src/EfficientDynamoDb/Exceptions/InternalServerErrorException.cs +++ b/src/EfficientDynamoDb/Exceptions/InternalServerErrorException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class InternalServerErrorException : RetryableException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.InternalServerError; + public InternalServerErrorException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/ItemCollectionSizeLimitExceededException.cs b/src/EfficientDynamoDb/Exceptions/ItemCollectionSizeLimitExceededException.cs index b07b96ea..890e4890 100644 --- a/src/EfficientDynamoDb/Exceptions/ItemCollectionSizeLimitExceededException.cs +++ b/src/EfficientDynamoDb/Exceptions/ItemCollectionSizeLimitExceededException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class ItemCollectionSizeLimitExceededException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.ItemCollectionSizeLimitExceeded; + public ItemCollectionSizeLimitExceededException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/LimitExceededException.cs b/src/EfficientDynamoDb/Exceptions/LimitExceededException.cs index 943c1669..d5d2ba36 100644 --- a/src/EfficientDynamoDb/Exceptions/LimitExceededException.cs +++ b/src/EfficientDynamoDb/Exceptions/LimitExceededException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class LimitExceededException : RetryableException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.LimitExceeded; + public LimitExceededException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/MissingAuthenticationTokenException.cs b/src/EfficientDynamoDb/Exceptions/MissingAuthenticationTokenException.cs index 524c57dd..64b8cd8b 100644 --- a/src/EfficientDynamoDb/Exceptions/MissingAuthenticationTokenException.cs +++ b/src/EfficientDynamoDb/Exceptions/MissingAuthenticationTokenException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class MissingAuthenticationTokenException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.MissingAuthenticationToken; + public MissingAuthenticationTokenException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/ProvisionedThroughputExceededException.cs b/src/EfficientDynamoDb/Exceptions/ProvisionedThroughputExceededException.cs index 9717a034..c289992f 100644 --- a/src/EfficientDynamoDb/Exceptions/ProvisionedThroughputExceededException.cs +++ b/src/EfficientDynamoDb/Exceptions/ProvisionedThroughputExceededException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class ProvisionedThroughputExceededException : RetryableException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.ProvisionedThroughputExceeded; + public ProvisionedThroughputExceededException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/RequestLimitExceededException.cs b/src/EfficientDynamoDb/Exceptions/RequestLimitExceededException.cs index 26019f4e..fe486273 100644 --- a/src/EfficientDynamoDb/Exceptions/RequestLimitExceededException.cs +++ b/src/EfficientDynamoDb/Exceptions/RequestLimitExceededException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -10,6 +10,10 @@ namespace EfficientDynamoDb.Exceptions /// public class RequestLimitExceededException : RetryableException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.RequestLimitExceeded; + public RequestLimitExceededException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/ResourceInUseException.cs b/src/EfficientDynamoDb/Exceptions/ResourceInUseException.cs index 5fd98247..26f69da5 100644 --- a/src/EfficientDynamoDb/Exceptions/ResourceInUseException.cs +++ b/src/EfficientDynamoDb/Exceptions/ResourceInUseException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -10,6 +10,10 @@ namespace EfficientDynamoDb.Exceptions /// public class ResourceInUseException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.ResourceInUse; + public ResourceInUseException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/ResourceNotFoundException.cs b/src/EfficientDynamoDb/Exceptions/ResourceNotFoundException.cs index 440d0449..f29b291f 100644 --- a/src/EfficientDynamoDb/Exceptions/ResourceNotFoundException.cs +++ b/src/EfficientDynamoDb/Exceptions/ResourceNotFoundException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -10,6 +10,10 @@ namespace EfficientDynamoDb.Exceptions /// public class ResourceNotFoundException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.ResourceNotFound; + public ResourceNotFoundException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/RetryableException.cs b/src/EfficientDynamoDb/Exceptions/RetryableException.cs index 355c75d3..583d670d 100644 --- a/src/EfficientDynamoDb/Exceptions/RetryableException.cs +++ b/src/EfficientDynamoDb/Exceptions/RetryableException.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; namespace EfficientDynamoDb.Exceptions { diff --git a/src/EfficientDynamoDb/Exceptions/ServiceUnavailableException.cs b/src/EfficientDynamoDb/Exceptions/ServiceUnavailableException.cs index 69abb949..25aa2c52 100644 --- a/src/EfficientDynamoDb/Exceptions/ServiceUnavailableException.cs +++ b/src/EfficientDynamoDb/Exceptions/ServiceUnavailableException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class ServiceUnavailableException : RetryableException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.ServiceUnavailable; + public ServiceUnavailableException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/ThrottlingException.cs b/src/EfficientDynamoDb/Exceptions/ThrottlingException.cs index caf9cf0e..fe9d54e1 100644 --- a/src/EfficientDynamoDb/Exceptions/ThrottlingException.cs +++ b/src/EfficientDynamoDb/Exceptions/ThrottlingException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -10,6 +10,10 @@ namespace EfficientDynamoDb.Exceptions /// public class ThrottlingException : RetryableException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.Throttling; + public ThrottlingException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/TransactionCanceledException.cs b/src/EfficientDynamoDb/Exceptions/TransactionCanceledException.cs index 83652be1..7bca6045 100644 --- a/src/EfficientDynamoDb/Exceptions/TransactionCanceledException.cs +++ b/src/EfficientDynamoDb/Exceptions/TransactionCanceledException.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.Shared; namespace EfficientDynamoDb.Exceptions @@ -52,6 +53,10 @@ namespace EfficientDynamoDb.Exceptions public class TransactionCanceledException : DdbException { public IReadOnlyList CancellationReasons { get; } + + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.TransactionCanceled; public TransactionCanceledException(IReadOnlyList cancellationReasons, string message) : base(message) { diff --git a/src/EfficientDynamoDb/Exceptions/TransactionInProgressException.cs b/src/EfficientDynamoDb/Exceptions/TransactionInProgressException.cs index 1124c277..4d5b2963 100644 --- a/src/EfficientDynamoDb/Exceptions/TransactionInProgressException.cs +++ b/src/EfficientDynamoDb/Exceptions/TransactionInProgressException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -8,6 +8,10 @@ namespace EfficientDynamoDb.Exceptions /// public class TransactionInProgressException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.TransactionInProgress; + public TransactionInProgressException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/UnrecognizedClientException.cs b/src/EfficientDynamoDb/Exceptions/UnrecognizedClientException.cs index 6027d86a..f4ed7eec 100644 --- a/src/EfficientDynamoDb/Exceptions/UnrecognizedClientException.cs +++ b/src/EfficientDynamoDb/Exceptions/UnrecognizedClientException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class UnrecognizedClientException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.UnrecognizedClient; + public UnrecognizedClientException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Exceptions/ValidationException.cs b/src/EfficientDynamoDb/Exceptions/ValidationException.cs index 58006ed7..870d776b 100644 --- a/src/EfficientDynamoDb/Exceptions/ValidationException.cs +++ b/src/EfficientDynamoDb/Exceptions/ValidationException.cs @@ -1,5 +1,5 @@ using System; -using System.Runtime.Serialization; +using EfficientDynamoDb.Operations; namespace EfficientDynamoDb.Exceptions { @@ -9,6 +9,10 @@ namespace EfficientDynamoDb.Exceptions /// public class ValidationException : DdbException { + internal override OpErrorType OpErrorType => ErrorType; + + internal static OpErrorType ErrorType => OpErrorType.Validation; + public ValidationException(string message) : base(message) { } diff --git a/src/EfficientDynamoDb/Operations/OpResult.cs b/src/EfficientDynamoDb/Operations/OpResult.cs new file mode 100644 index 00000000..5ad53089 --- /dev/null +++ b/src/EfficientDynamoDb/Operations/OpResult.cs @@ -0,0 +1,202 @@ +using System; +using System.Runtime.InteropServices; +using EfficientDynamoDb.Exceptions; + +namespace EfficientDynamoDb.Operations +{ + // TODO: Apply proper nullability attributes after migration to .net8 + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis + [StructLayout(LayoutKind.Auto)] + public readonly struct OpResult + { + public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; + + public DdbException? Exception { get; } + + public OpResult(DdbException? exception) + { + Exception = exception; + } + + public void EnsureSuccess() + { + if (Exception is not null) + throw Exception; + } + + public bool IsSuccess => Exception is null; + + public ServiceUnavailableException AsServiceUnavailableException() => + AsException(ServiceUnavailableException.ErrorType); + + public InternalServerErrorException AsInternalServerErrorException() => + AsException(InternalServerErrorException.ErrorType); + + public TransactionCanceledException AsTransactionCanceledException() => + AsException(TransactionCanceledException.ErrorType); + + public ConditionalCheckFailedException AsConditionalCheckFailedException() => + AsException(ConditionalCheckFailedException.ErrorType); + + public ProvisionedThroughputExceededException AsProvisionedThroughputExceededException() => + AsException(ProvisionedThroughputExceededException.ErrorType); + + public AccessDeniedException AsAccessDeniedException() => + AsException(AccessDeniedException.ErrorType); + + public IncompleteSignatureException AsIncompleteSignatureException() => + AsException(IncompleteSignatureException.ErrorType); + + public ItemCollectionSizeLimitExceededException AsItemCollectionSizeLimitExceededException() => + AsException(ItemCollectionSizeLimitExceededException.ErrorType); + + public LimitExceededException AsLimitExceededException() => + AsException(LimitExceededException.ErrorType); + + public MissingAuthenticationTokenException AsMissingAuthenticationTokenException() => + AsException(MissingAuthenticationTokenException.ErrorType); + + public RequestLimitExceededException AsRequestLimitExceededException() => + AsException(RequestLimitExceededException.ErrorType); + + public ResourceInUseException AsResourceInUseException() => + AsException(ResourceInUseException.ErrorType); + + public ResourceNotFoundException AsResourceNotFoundException() => + AsException(ResourceNotFoundException.ErrorType); + + public ThrottlingException AsThrottlingException() => + AsException(ThrottlingException.ErrorType); + + public UnrecognizedClientException AsUnrecognizedClientException() => + AsException(UnrecognizedClientException.ErrorType); + + public ValidationException AsValidationException() => + AsException(ValidationException.ErrorType); + + public IdempotentParameterMismatchException AsIdempotentParameterMismatchException() => + AsException(IdempotentParameterMismatchException.ErrorType); + + public TransactionInProgressException AsTransactionInProgressException() => + AsException(TransactionInProgressException.ErrorType); + + private T AsException(OpErrorType expectedType) where T : DdbException => Exception switch + { + T ex => ex, + _ => throw new InvalidOperationException($"Operation error contains '{ErrorType}' value instead of '{expectedType}'.") + }; + } + + public readonly struct OpResult + { + public T? Value { get; } + + public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; + + public DdbException? Exception { get; } + + public OpResult(DdbException exception) + { + Value = default; + Exception = exception; + } + + public OpResult(T value) + { + Value = value; + Exception = null; + } + + public void EnsureSuccess() + { + if (ErrorType != OpErrorType.None && Exception is not null) + throw Exception; + } + + public bool IsSuccess => Exception is null; + + public ServiceUnavailableException AsServiceUnavailableException() => + AsException(ServiceUnavailableException.ErrorType); + + public InternalServerErrorException AsInternalServerErrorException() => + AsException(InternalServerErrorException.ErrorType); + + public TransactionCanceledException AsTransactionCanceledException() => + AsException(TransactionCanceledException.ErrorType); + + public ConditionalCheckFailedException AsConditionalCheckFailedException() => + AsException(ConditionalCheckFailedException.ErrorType); + + public ProvisionedThroughputExceededException AsProvisionedThroughputExceededException() => + AsException(ProvisionedThroughputExceededException.ErrorType); + + public AccessDeniedException AsAccessDeniedException() => + AsException(AccessDeniedException.ErrorType); + + public IncompleteSignatureException AsIncompleteSignatureException() => + AsException(IncompleteSignatureException.ErrorType); + + public ItemCollectionSizeLimitExceededException AsItemCollectionSizeLimitExceededException() => + AsException(ItemCollectionSizeLimitExceededException.ErrorType); + + public LimitExceededException AsLimitExceededException() => + AsException(LimitExceededException.ErrorType); + + public MissingAuthenticationTokenException AsMissingAuthenticationTokenException() => + AsException(MissingAuthenticationTokenException.ErrorType); + + public RequestLimitExceededException AsRequestLimitExceededException() => + AsException(RequestLimitExceededException.ErrorType); + + public ResourceInUseException AsResourceInUseException() => + AsException(ResourceInUseException.ErrorType); + + public ResourceNotFoundException AsResourceNotFoundException() => + AsException(ResourceNotFoundException.ErrorType); + + public ThrottlingException AsThrottlingException() => + AsException(ThrottlingException.ErrorType); + + public UnrecognizedClientException AsUnrecognizedClientException() => + AsException(UnrecognizedClientException.ErrorType); + + public ValidationException AsValidationException() => + AsException(ValidationException.ErrorType); + + public IdempotentParameterMismatchException AsIdempotentParameterMismatchException() => + AsException(IdempotentParameterMismatchException.ErrorType); + + public TransactionInProgressException AsTransactionInProgressException() => + AsException(TransactionInProgressException.ErrorType); + + private TException AsException(OpErrorType expectedType) where TException : DdbException => Exception switch + { + TException ex => ex, + _ => throw new InvalidOperationException($"Operation error contains '{ErrorType}' value instead of '{expectedType}'.") + }; + } + + public enum OpErrorType + { + None, + Unknown, + ServiceUnavailable, + InternalServerError, + TransactionCanceled, + ConditionalCheckFailed, + ProvisionedThroughputExceeded, + AccessDenied, + IncompleteSignature, + ItemCollectionSizeLimitExceeded, + LimitExceeded, + MissingAuthenticationToken, + RequestLimitExceeded, + ResourceInUse, + ResourceNotFound, + Throttling, + UnrecognizedClient, + Validation, + IdempotentParameterMismatch, + TransactionInProgress + } +} \ No newline at end of file From a23c73428cde60a990faa71359fd710dd43e2d5d Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sun, 8 Sep 2024 20:55:28 +0200 Subject: [PATCH 03/28] WIP: HttpAPI --- .../Internal/ErrorHandler.cs | 40 +++++++++---------- src/EfficientDynamoDb/Internal/HttpApi.cs | 20 ++++++++-- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/EfficientDynamoDb/Internal/ErrorHandler.cs b/src/EfficientDynamoDb/Internal/ErrorHandler.cs index 9cd30ec4..7a46e49d 100644 --- a/src/EfficientDynamoDb/Internal/ErrorHandler.cs +++ b/src/EfficientDynamoDb/Internal/ErrorHandler.cs @@ -22,7 +22,7 @@ internal static class ErrorHandler PropertyNameCaseInsensitive = true, }; - public static async Task ProcessErrorAsync(DynamoDbContextMetadata metadata, HttpResponseMessage response, CancellationToken cancellationToken = default) + public static async Task ProcessErrorAsync(DynamoDbContextMetadata metadata, HttpResponseMessage response, CancellationToken cancellationToken = default) { try { @@ -60,10 +60,10 @@ public static async Task ProcessErrorAsync(DynamoDbContextMetadata me } } - private static ValueTask ProcessBadRequestAsync(DynamoDbContextMetadata metadata, MemoryStream recyclableStream, Error error, CancellationToken cancellationToken) + private static ValueTask ProcessBadRequestAsync(DynamoDbContextMetadata metadata, MemoryStream recyclableStream, Error error, CancellationToken cancellationToken) { if (error.Type is null) - return new ValueTask(new DdbException(string.Empty)); + return new(new DdbException(string.Empty)); var exceptionStart = error.Type.LastIndexOf('#'); var type = exceptionStart != -1 ? error.Type.AsSpan(exceptionStart + 1) : error.Type.AsSpan(); @@ -75,37 +75,37 @@ private static ValueTask ProcessBadRequestAsync(DynamoDbContextMetada return ParseConditionalCheckFailedException(); if (type.Equals("ProvisionedThroughputExceededException", StringComparison.Ordinal)) - return new ValueTask(new ProvisionedThroughputExceededException(error.Message)); + return new(new ProvisionedThroughputExceededException(error.Message)); if (type.Equals("AccessDeniedException", StringComparison.Ordinal)) - return new ValueTask(new AccessDeniedException(error.Message)); + return new(new AccessDeniedException(error.Message)); if (type.Equals("IncompleteSignatureException", StringComparison.Ordinal)) - return new ValueTask(new IncompleteSignatureException(error.Message)); + return new(new IncompleteSignatureException(error.Message)); if (type.Equals("ItemCollectionSizeLimitExceededException", StringComparison.Ordinal)) - return new ValueTask(new ItemCollectionSizeLimitExceededException(error.Message)); + return new(new ItemCollectionSizeLimitExceededException(error.Message)); if (type.Equals("LimitExceededException", StringComparison.Ordinal)) - return new ValueTask(new LimitExceededException(error.Message)); + return new(new LimitExceededException(error.Message)); if (type.Equals("MissingAuthenticationTokenException", StringComparison.Ordinal)) - return new ValueTask(new MissingAuthenticationTokenException(error.Message)); + return new(new MissingAuthenticationTokenException(error.Message)); if (type.Equals("RequestLimitExceeded", StringComparison.Ordinal)) - return new ValueTask(new RequestLimitExceededException(error.Message)); + return new(new RequestLimitExceededException(error.Message)); if (type.Equals("ResourceInUseException", StringComparison.Ordinal)) - return new ValueTask(new ResourceInUseException(error.Message)); + return new(new ResourceInUseException(error.Message)); if (type.Equals("ResourceNotFoundException", StringComparison.Ordinal)) - return new ValueTask(new ResourceNotFoundException(error.Message)); + return new(new ResourceNotFoundException(error.Message)); if (type.Equals("ThrottlingException", StringComparison.Ordinal)) - return new ValueTask(new ThrottlingException(error.Message)); + return new(new ThrottlingException(error.Message)); if (type.Equals("UnrecognizedClientException", StringComparison.Ordinal)) - return new ValueTask(new UnrecognizedClientException(error.Message)); + return new(new UnrecognizedClientException(error.Message)); if (type.Equals("ValidationException", StringComparison.Ordinal)) - return new ValueTask(new ValidationException(error.Message)); + return new(new ValidationException(error.Message)); if (type.Equals("IdempotentParameterMismatchException", StringComparison.Ordinal)) - return new ValueTask(new IdempotentParameterMismatchException(error.Message)); + return new(new IdempotentParameterMismatchException(error.Message)); if (type.Equals("TransactionInProgressException", StringComparison.Ordinal)) - return new ValueTask(new TransactionInProgressException(error.Message)); + return new(new TransactionInProgressException(error.Message)); - return new ValueTask(new DdbException(error.Message ?? type.ToString())); + return new(new DdbException(error.Message ?? type.ToString())); - async ValueTask ParseTransactionCancelledException() + async ValueTask ParseTransactionCancelledException() { var classInfo = metadata.GetOrAddClassInfo(typeof(TransactionCancelledResponse), typeof(JsonObjectDdbConverter)); var transactionCancelledResponse = await EntityDdbJsonReader.ReadAsync(recyclableStream, classInfo, metadata, @@ -113,7 +113,7 @@ async ValueTask ParseTransactionCancelledException() return new TransactionCanceledException(transactionCancelledResponse.Value!.CancellationReasons, error.Message); } - async ValueTask ParseConditionalCheckFailedException() + async ValueTask ParseConditionalCheckFailedException() { var classInfo = metadata.GetOrAddClassInfo(typeof(ConditionalCheckFailedResponse), typeof(JsonObjectDdbConverter)); diff --git a/src/EfficientDynamoDb/Internal/HttpApi.cs b/src/EfficientDynamoDb/Internal/HttpApi.cs index 81fe3acf..cceab349 100644 --- a/src/EfficientDynamoDb/Internal/HttpApi.cs +++ b/src/EfficientDynamoDb/Internal/HttpApi.cs @@ -20,7 +20,18 @@ public HttpApi(IHttpClientFactory httpClientFactory) _httpClientFactory = httpClientFactory; } - public async ValueTask SendAsync(DynamoDbContextConfig config, HttpContent httpContent, CancellationToken cancellationToken = default) + public async ValueTask SendAsync(DynamoDbContextConfig config, HttpContent httpContent, + CancellationToken cancellationToken = default) + { + var (response, exception) = await SendSafeAsync(config, httpContent, cancellationToken).ConfigureAwait(false); + if (exception != null) + throw exception; + + return response!; + } + + public async ValueTask<(HttpResponseMessage? Response, DdbException? Exception)> SendSafeAsync(DynamoDbContextConfig config, HttpContent httpContent, + CancellationToken cancellationToken = default) { try { @@ -47,9 +58,10 @@ public async ValueTask SendAsync(DynamoDbContextConfig conf var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); if (response.IsSuccessStatusCode) - return response; - + return (response, null); + var error = await ErrorHandler.ProcessErrorAsync(config.Metadata, response, cancellationToken).ConfigureAwait(false); + switch (error) { case ProvisionedThroughputExceededException when config.RetryStrategies.ProvisionedThroughputExceededStrategy.TryGetRetryDelay(provisionedThroughputExceededRetries++, out var delay): @@ -61,7 +73,7 @@ public async ValueTask SendAsync(DynamoDbContextConfig conf await Task.Delay(delay, cancellationToken).ConfigureAwait(false); break; case not null: - throw error; + return (null, error); } } finally From 8d91c1833da31b48e18fc408fd5c92ec2c96e581 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Mon, 9 Sep 2024 01:17:09 +0200 Subject: [PATCH 04/28] GetItem --- .../DynamoDbContext.GetItem.cs | 37 ++++-- .../GetItem/GetItemRequestBuilder.cs | 107 ++++++++++++++++-- .../GetItem/IGetItemRequestBuilder.cs | 89 ++++++++++++++- src/EfficientDynamoDb/Operations/OpResult.cs | 14 ++- 4 files changed, 224 insertions(+), 23 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.GetItem.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.GetItem.cs index 92fc7f76..d2eae948 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.GetItem.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.GetItem.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using EfficientDynamoDb.Internal.Metadata; using EfficientDynamoDb.Internal.Operations.GetItem; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.GetItem; using EfficientDynamoDb.Operations.Query; @@ -74,7 +75,11 @@ public partial class DynamoDbContext /// If the table has both partition and sort keys, use instead. /// public Task GetItemAsync(TPartitionKey partitionKey, CancellationToken cancellationToken = default) - where TEntity : class => GetItemAsync(Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), new PartitionKeyNode(partitionKey, null), cancellationToken); + where TEntity : class => GetItemAsync( + Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), + new PartitionKeyNode(partitionKey, null), + cancellationToken) + .EnsureSuccess(); /// /// Executes GetItem operation asynchronously. @@ -91,25 +96,39 @@ public partial class DynamoDbContext /// If the table has only a partition key, use instead. /// public Task GetItemAsync(TPartitionKey partitionKey, TSortKey sortKey, - CancellationToken cancellationToken = default) where TEntity : class => - GetItemAsync(Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), new PartitionAndSortKeyNode(partitionKey, sortKey, null), cancellationToken); + CancellationToken cancellationToken = default) where TEntity : class => + GetItemAsync( + Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), + new PartitionAndSortKeyNode(partitionKey, sortKey, null), + cancellationToken) + .EnsureSuccess(); - internal async Task GetItemAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task> GetItemAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new GetItemHighLevelHttpContent(this, classInfo, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); - return result.Item; + return new(result.Item); } - internal async Task> GetItemResponseAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> GetItemResponseAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new GetItemHighLevelHttpContent(this, classInfo, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - return await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + + return new(result); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/GetItem/GetItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/GetItem/GetItemRequestBuilder.cs index b5dffd63..3a32e56f 100644 --- a/src/EfficientDynamoDb/Operations/GetItem/GetItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/GetItem/GetItemRequestBuilder.cs @@ -52,22 +52,50 @@ public IGetItemEntityRequestBuilder WithProjectedAttributes(params Expr new GetItemEntityRequestBuilder(_context, new ProjectedAttributesNode(typeof(TEntity), properties, _node)); public IGetItemDocumentRequestBuilder AsDocument() => new GetItemDocumentRequestBuilder(_context, _node); + + public ISuppressedGetItemEntityRequestBuilder SuppressThrowing() => new SuppressedGetItemEntityRequestBuilder(_context, _node); public async Task ToItemAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } - private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch get item request."); + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty get item request."); } - + + internal sealed class SuppressedGetItemEntityRequestBuilder : ISuppressedGetItemEntityRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedGetItemEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task> ToItemAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty get item request."); + } + internal sealed class GetItemEntityRequestBuilder : IGetItemEntityRequestBuilder where TEntity : class where TProjection : class { private readonly DynamoDbContext _context; @@ -102,22 +130,53 @@ public IGetItemEntityRequestBuilder WithPrimaryKey(TP new GetItemEntityRequestBuilder(_context, new PartitionKeyNode(pk, _node)); public IGetItemDocumentRequestBuilder AsDocument() => new GetItemDocumentRequestBuilder(_context, _node); + + public ISuppressedGetItemEntityRequestBuilder SuppressThrowing() => + new SuppressedGetItemEntityRequestBuilder(_context, _node); public async Task ToItemAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch get item request."); } - + + internal sealed class SuppressedGetItemEntityRequestBuilder : ISuppressedGetItemEntityRequestBuilder + where TEntity : class + where TProjection : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedGetItemEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task> ToItemAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty get item request."); + } + internal sealed class GetItemDocumentRequestBuilder : IGetItemDocumentRequestBuilder where TEntity : class { private readonly DynamoDbContext _context; @@ -160,19 +219,47 @@ public IGetItemDocumentRequestBuilder WithPrimaryKey(TPk pk, public IGetItemDocumentRequestBuilder WithPrimaryKey(TPk pk) => new GetItemDocumentRequestBuilder(_context, new PartitionKeyNode(pk, _node)); - + public ISuppressedGetItemDocumentRequestBuilder SuppressThrowing() => new SuppressedGetItemDocumentRequestBuilder(_context, _node); + public async Task ToItemAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch get item request."); } + + internal sealed class SuppressedGetItemDocumentRequestBuilder : ISuppressedGetItemDocumentRequestBuilder + where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedGetItemDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task> ToItemAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.GetItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.GetItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty get item request."); + } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/GetItem/IGetItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/GetItem/IGetItemRequestBuilder.cs index bfe9f811..d2500d4f 100644 --- a/src/EfficientDynamoDb/Operations/GetItem/IGetItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/GetItem/IGetItemRequestBuilder.cs @@ -93,6 +93,11 @@ public interface IGetItemEntityRequestBuilder : ITableBuilderGetItem operation builder suitable for document response. IGetItemDocumentRequestBuilder AsDocument(); + /// + /// Suppresses the throwing of the exceptions related to the GetItem operation. + /// + ISuppressedGetItemEntityRequestBuilder SuppressThrowing(); + /// /// Executes the GetItem operation and returns the item. /// @@ -110,13 +115,38 @@ public interface IGetItemEntityRequestBuilder : ITableBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } - + + public interface ISuppressedGetItemEntityRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the GetItem operation and returns the operation result with an item. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// will be null if the item does not exist. + ///
+ /// This method returns DynamoDB related exceptions instead of throwing them. + ///
+ Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the GetItem operation and returns the operation result with deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// This method returns DynamoDB related exceptions instead of throwing them. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } + /// /// Represents a builder for the projected GetItem operation. /// /// Type of the DB entity. /// Type of the projection. - public interface IGetItemEntityRequestBuilder : ITableBuilder> where TEntity : class where TProjection : class + public interface IGetItemEntityRequestBuilder : ITableBuilder> + where TEntity : class + where TProjection : class { /// IGetItemEntityRequestBuilder WithConsistentRead(bool useConsistentRead); @@ -143,6 +173,11 @@ public interface IGetItemEntityRequestBuilder : ITableBuil ///
/// GetItem operation builder suitable for document response. IGetItemDocumentRequestBuilder AsDocument(); + + /// + /// Suppresses the throwing of the exceptions related to the GetItem operation. + /// + ISuppressedGetItemEntityRequestBuilder SuppressThrowing(); /// /// Executes the GetItem operation and returns the projected item. @@ -161,7 +196,32 @@ public interface IGetItemEntityRequestBuilder : ITableBuil /// A task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } - + + public interface ISuppressedGetItemEntityRequestBuilder : ITableBuilder> + where TEntity : class + where TProjection : class + { + /// + /// Executes the GetItem operation and returns the operation result with projected item. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// will be null if the item does not exist. + ///
+ /// This method returns DynamoDB related exceptions instead of throwing them. + ///
+ Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the GetItem operation and returns the operation result with projected deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// This method returns DynamoDB related exceptions instead of throwing them. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } + public interface IGetItemDocumentRequestBuilder : ITableBuilder> where TEntity : class { /// @@ -223,4 +283,27 @@ public interface IGetItemDocumentRequestBuilder : ITableBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } + + public interface ISuppressedGetItemDocumentRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the GetItem operation and returns the operation result with item attributes. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// will be null if the item does not exist. + ///
+ /// This method returns DynamoDB related exceptions instead of throwing them. + ///
+ Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the GetItem operation and returns the operation result with projected deserialized response with item attributes. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// This method returns DynamoDB related exceptions instead of throwing them. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/OpResult.cs b/src/EfficientDynamoDb/Operations/OpResult.cs index 5ad53089..663637e8 100644 --- a/src/EfficientDynamoDb/Operations/OpResult.cs +++ b/src/EfficientDynamoDb/Operations/OpResult.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Threading.Tasks; using EfficientDynamoDb.Exceptions; namespace EfficientDynamoDb.Operations @@ -107,10 +108,12 @@ public OpResult(T value) Exception = null; } - public void EnsureSuccess() + public T EnsureSuccess() { if (ErrorType != OpErrorType.None && Exception is not null) throw Exception; + + return Value!; } public bool IsSuccess => Exception is null; @@ -176,6 +179,15 @@ public TransactionInProgressException AsTransactionInProgressException() => }; } + internal static class OpResultExtensions + { + public static async Task EnsureSuccess(this Task> task) + { + var result = await task.ConfigureAwait(false); + return result.EnsureSuccess(); + } + } + public enum OpErrorType { None, From 8f2fd1f040c036de5bfcde5fe698d19aeb0b8fa7 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 14 Sep 2024 13:53:40 +0200 Subject: [PATCH 05/28] PutItem --- .../DynamoDbContext.PutItem.cs | 20 ++++-- src/EfficientDynamoDb/Operations/OpResult.cs | 2 + .../PutItem/IPutItemRequestBuilder.cs | 68 ++++++++++++++++++- .../PutItem/PutItemRequestBuilder.cs | 68 ++++++++++++++++--- 4 files changed, 143 insertions(+), 15 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.PutItem.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.PutItem.cs index af4a5c6d..56680058 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.PutItem.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.PutItem.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using EfficientDynamoDb.Internal.Operations.PutItem; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.PutItem; using EfficientDynamoDb.Operations.Query; @@ -26,25 +27,32 @@ public Task PutItemAsync(TEntity item, CancellationToken cancellationTo return PutItem().WithItem(item).ExecuteAsync(cancellationToken); } - internal async Task> PutItemResponseAsync(BuilderNode? node, + internal async Task>> PutItemResponseAsync(BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new PutItemHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); - return await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + return new(result); } - internal async Task PutItemAsync(BuilderNode? node, + internal async Task> PutItemAsync(BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new PutItemHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); - return result.Item; + return new(result.Item); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/OpResult.cs b/src/EfficientDynamoDb/Operations/OpResult.cs index 663637e8..667d3e48 100644 --- a/src/EfficientDynamoDb/Operations/OpResult.cs +++ b/src/EfficientDynamoDb/Operations/OpResult.cs @@ -117,6 +117,8 @@ public T EnsureSuccess() } public bool IsSuccess => Exception is null; + + public OpResult DiscardValue() => new(Exception); public ServiceUnavailableException AsServiceUnavailableException() => AsException(ServiceUnavailableException.ErrorType); diff --git a/src/EfficientDynamoDb/Operations/PutItem/IPutItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/PutItem/IPutItemRequestBuilder.cs index bd0e5437..2ce455df 100644 --- a/src/EfficientDynamoDb/Operations/PutItem/IPutItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/PutItem/IPutItemRequestBuilder.cs @@ -56,7 +56,7 @@ public interface IPutItemRequestBuilder : ITableBuilder /// IPutItemRequestBuilder WithCondition(FilterBase condition); } - + /// /// Represents a builder for the PutItem operation with an entity type constraint. /// Provides methods for configuring options and executing the operation with typed response. @@ -115,6 +115,11 @@ public interface IPutItemEntityRequestBuilder : ITableBuilder /// PutItem operation builder suitable for document response. IPutItemDocumentRequestBuilder AsDocument(); + + /// + /// Suppresses the throwing of the exceptions related to the PutItem operation. + /// + ISuppressedPutItemEntityRequestBuilder SuppressThrowing(); /// /// Executes the PutItem operation. @@ -143,6 +148,34 @@ public interface IPutItemEntityRequestBuilder : ITableBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the PutItem operation and returns the operation result. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the PutItem operation and returns the operation result with item before the update. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// The item is returned as it appeared before the PutItem operation, but only if with was specified in the request chain. + /// Otherwise, null is returned. + /// + Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the PutItem operation and returns the operation results with deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } + /// /// Represents a builder for the PutItem operation with an entity type constraint and a document response. /// Provides methods for configuring options and executing the operation with a representation of the response. @@ -189,6 +222,11 @@ public interface IPutItemDocumentRequestBuilder : ITableBuilder IPutItemDocumentRequestBuilder WithCondition(Func, FilterBase> conditionSetup); + /// + /// Suppresses the throwing of the exceptions related to the PutItem operation. + /// + ISuppressedPutItemDocumentRequestBuilder SuppressThrowing(); + /// /// Executes the PutItem operation. /// @@ -214,4 +252,32 @@ public interface IPutItemDocumentRequestBuilder : ITableBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } + + public interface ISuppressedPutItemDocumentRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the PutItem operation and returns the operation result. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the PutItem operation and returns the operation result with item's attributes before the update. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// The item is returned as it appeared before the PutItem operation, but only if with was specified in the request chain. + /// Otherwise, null is returned. + /// + Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the PutItem operation and returns the operation results with deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/PutItem/PutItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/PutItem/PutItemRequestBuilder.cs index 5e09faa4..75ef2754 100644 --- a/src/EfficientDynamoDb/Operations/PutItem/PutItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/PutItem/PutItemRequestBuilder.cs @@ -64,13 +64,13 @@ internal PutItemEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) } public async Task ExecuteAsync(CancellationToken cancellationToken = default) => - await _context.PutItemAsync(_node, cancellationToken).ConfigureAwait(false); + await _context.PutItemAsync(_node, cancellationToken).EnsureSuccess().ConfigureAwait(false); public Task ToItemAsync(CancellationToken cancellationToken = default) => - _context.PutItemAsync(_node, cancellationToken); + _context.PutItemAsync(_node, cancellationToken).EnsureSuccess(); public Task> ToResponseAsync(CancellationToken cancellationToken = default) => - _context.PutItemResponseAsync(_node, cancellationToken); + _context.PutItemResponseAsync(_node, cancellationToken).EnsureSuccess(); public IPutItemEntityRequestBuilder WithReturnValues(ReturnValues returnValues) => new PutItemEntityRequestBuilder(_context, new ReturnValuesNode(returnValues, _node)); @@ -91,9 +91,35 @@ public IPutItemEntityRequestBuilder WithCondition(Func(_context, new ConditionNode(conditionSetup(Condition.ForEntity()), _node)); public IPutItemDocumentRequestBuilder AsDocument() => new PutItemDocumentRequestBuilder(_context, _node); + + public ISuppressedPutItemEntityRequestBuilder SuppressThrowing() => new SuppressedPutItemEntityRequestBuilder(_context, _node); } - - internal sealed class PutItemDocumentRequestBuilder : IPutItemDocumentRequestBuilder where TEntity : class + + internal sealed class SuppressedPutItemEntityRequestBuilder : ISuppressedPutItemEntityRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedPutItemEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var result = await _context.PutItemAsync(_node, cancellationToken).ConfigureAwait(false); + return result.DiscardValue(); + } + + public Task> ToItemAsync(CancellationToken cancellationToken = default) => + _context.PutItemAsync(_node, cancellationToken); + + public Task>> ToResponseAsync(CancellationToken cancellationToken = default) => + _context.PutItemResponseAsync(_node, cancellationToken); + } + + internal sealed class PutItemDocumentRequestBuilder : IPutItemDocumentRequestBuilder where TEntity : class { private readonly DynamoDbContext _context; private readonly BuilderNode? _node; @@ -112,13 +138,13 @@ internal PutItemDocumentRequestBuilder(DynamoDbContext context, BuilderNode? nod } public async Task ExecuteAsync(CancellationToken cancellationToken = default) => - await _context.PutItemAsync(_node, cancellationToken).ConfigureAwait(false); + await _context.PutItemAsync(_node, cancellationToken).EnsureSuccess().ConfigureAwait(false); public Task ToItemAsync(CancellationToken cancellationToken = default) => - _context.PutItemAsync(_node, cancellationToken); + _context.PutItemAsync(_node, cancellationToken).EnsureSuccess(); public Task> ToResponseAsync(CancellationToken cancellationToken = default) => - _context.PutItemResponseAsync(_node, cancellationToken); + _context.PutItemResponseAsync(_node, cancellationToken).EnsureSuccess(); public IPutItemDocumentRequestBuilder WithReturnValues(ReturnValues returnValues) => new PutItemDocumentRequestBuilder(_context, new ReturnValuesNode(returnValues, _node)); @@ -134,5 +160,31 @@ public IPutItemDocumentRequestBuilder WithCondition(FilterBase conditio public IPutItemDocumentRequestBuilder WithCondition(Func, FilterBase> conditionSetup) => new PutItemDocumentRequestBuilder(_context, new ConditionNode(conditionSetup(Condition.ForEntity()), _node)); + + public ISuppressedPutItemDocumentRequestBuilder SuppressThrowing() => new SuppressedPutItemDocumentRequestBuilder(_context, _node); + } + + internal sealed class SuppressedPutItemDocumentRequestBuilder : ISuppressedPutItemDocumentRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedPutItemDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var result = await _context.PutItemAsync(_node, cancellationToken).ConfigureAwait(false); + return result.DiscardValue(); + } + + public Task> ToItemAsync(CancellationToken cancellationToken = default) => + _context.PutItemAsync(_node, cancellationToken); + + public Task>> ToResponseAsync(CancellationToken cancellationToken = default) => + _context.PutItemResponseAsync(_node, cancellationToken); } } \ No newline at end of file From 4ddc84632297697eed8c8cb5fdeedc5cd290f64b Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 14 Sep 2024 14:35:26 +0200 Subject: [PATCH 06/28] DeleteItem --- .../DynamoDbContext.DeleteItem.cs | 32 +++++--- .../DeleteItem/DeleteItemRequestBuilder.cs | 70 +++++++++++++++-- .../DeleteItem/IDeleteItemRequestBuilder.cs | 75 ++++++++++++++++++- src/EfficientDynamoDb/Operations/OpResult.cs | 6 ++ 4 files changed, 165 insertions(+), 18 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.DeleteItem.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.DeleteItem.cs index 83803872..353ff61b 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.DeleteItem.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.DeleteItem.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using EfficientDynamoDb.Internal.Metadata; using EfficientDynamoDb.Internal.Operations.DeleteItem; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.DeleteItem; using EfficientDynamoDb.Operations.Query; @@ -55,34 +56,45 @@ public async Task DeleteItemAsync(object partitionKey, object sortKey, await ReadAsync(response, cancellationToken).ConfigureAwait(false); } - internal async Task> DeleteItemResponseAsync(DdbClassInfo classInfo, BuilderNode node, + internal async Task>> DeleteItemResponseAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new DeleteItemHighLevelHttpContent(this, classInfo, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); - return await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + return new(result); } - internal async Task DeleteItemAsync(DdbClassInfo classInfo, BuilderNode node, + internal async Task> DeleteItemAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new DeleteItemHighLevelHttpContent(this, classInfo, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); - return result.Attributes; + return new(result.Attributes); } - internal async Task DeleteItemAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) + internal async Task DeleteItemAsync(DdbClassInfo classInfo, BuilderNode node, CancellationToken cancellationToken = default) { using var httpContent = new DeleteItemHighLevelHttpContent(this, classInfo, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; await ReadAsync(response, cancellationToken).ConfigureAwait(false); + return new(); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DeleteItem/DeleteItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/DeleteItem/DeleteItemRequestBuilder.cs index 52d08113..b1883de0 100644 --- a/src/EfficientDynamoDb/Operations/DeleteItem/DeleteItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/DeleteItem/DeleteItemRequestBuilder.cs @@ -33,19 +33,19 @@ private DeleteItemEntityRequestBuilder(DynamoDbContext context, BuilderNode? nod public async Task ExecuteAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task ToItemAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.DeleteItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.DeleteItemResponseAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IDeleteItemEntityRequestBuilder WithCondition(FilterBase condition) => @@ -72,11 +72,39 @@ public IDeleteItemEntityRequestBuilder WithReturnConsumedCapacity(Retur public IDeleteItemEntityRequestBuilder WithReturnCollectionMetrics(ReturnItemCollectionMetrics returnItemCollectionMetrics) => new DeleteItemEntityRequestBuilder(_context, new ReturnItemCollectionMetricsNode(returnItemCollectionMetrics, _node)); + public ISuppressedDeleteItemEntityRequestBuilder SuppressThrowing() => new SuppressedDeleteItemEntityRequestBuilder(_context, _node); + public IDeleteItemDocumentRequestBuilder AsDocument() => new DeleteItemDocumentRequestBuilder(_context, _node); private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty delete item request."); } - + + internal sealed class SuppressedDeleteItemEntityRequestBuilder : ISuppressedDeleteItemEntityRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedDeleteItemEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public Task> ToItemAsync(CancellationToken cancellationToken = default) => + _context.DeleteItemAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), GetNode(), cancellationToken); + + public Task>> ToResponseAsync(CancellationToken cancellationToken = default) => + _context.DeleteItemResponseAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), GetNode(), cancellationToken); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty delete item request."); + } + internal sealed class DeleteItemDocumentRequestBuilder : IDeleteItemDocumentRequestBuilder where TEntity : class { private readonly DynamoDbContext _context; @@ -101,19 +129,19 @@ internal DeleteItemDocumentRequestBuilder(DynamoDbContext context, BuilderNode? public async Task ExecuteAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task ToItemAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.DeleteItemResponseAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.DeleteItemResponseAsync(classInfo, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IDeleteItemDocumentRequestBuilder WithCondition(FilterBase condition) => @@ -137,6 +165,34 @@ public IDeleteItemDocumentRequestBuilder WithReturnConsumedCapacity(Ret public IDeleteItemDocumentRequestBuilder WithReturnCollectionMetrics(ReturnItemCollectionMetrics returnItemCollectionMetrics) => new DeleteItemDocumentRequestBuilder(_context, new ReturnItemCollectionMetricsNode(returnItemCollectionMetrics, _node)); + public ISuppressedDeleteItemDocumentRequestBuilder SuppressThrowing() => new SuppressedDeleteItemDocumentRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty delete item request."); + } + + internal sealed class SuppressedDeleteItemDocumentRequestBuilder : ISuppressedDeleteItemDocumentRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedDeleteItemDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + return await _context.DeleteItemAsync(classInfo, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public Task> ToItemAsync(CancellationToken cancellationToken = default) => + _context.DeleteItemAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), GetNode(), cancellationToken); + + public Task>> ToResponseAsync(CancellationToken cancellationToken = default) => + _context.DeleteItemResponseAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), GetNode(), cancellationToken); + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty delete item request."); } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DeleteItem/IDeleteItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/DeleteItem/IDeleteItemRequestBuilder.cs index 39d468c5..d03f45b0 100644 --- a/src/EfficientDynamoDb/Operations/DeleteItem/IDeleteItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/DeleteItem/IDeleteItemRequestBuilder.cs @@ -91,6 +91,11 @@ public interface IDeleteItemEntityRequestBuilder : ITableBuilderDeleteItem operation builder suitable for document response. IDeleteItemDocumentRequestBuilder AsDocument(); + /// + /// Suppresses the throwing of the exceptions related to the DeleteItem operation. + /// + ISuppressedDeleteItemEntityRequestBuilder SuppressThrowing(); + /// /// Executes the DeleteItem operation. /// @@ -116,7 +121,41 @@ public interface IDeleteItemEntityRequestBuilder : ITableBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } - + + /// + /// Represents a builder for the DeleteItem operation with an entity type constraint. + /// Provides methods for configuring options and executing the operation with typed response. + /// + /// The type of the entity. + public interface ISuppressedDeleteItemEntityRequestBuilder : ITableBuilder> + where TEntity : class + { + /// + /// Executes the DeleteItem operation and returns the operation result. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the DeleteItem operation and returns the operation result with item's attributes before the delete. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// The item is returned as it appeared before the DeleteItem operation, but only if with was specified in the request chain. + /// Otherwise, null is returned. + /// + Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the DeleteItem operation and returns the operation results with deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } + /// /// Represents a builder for the DeleteItem operation with an entity type constraint and a document response. /// Provides methods for configuring options and executing the operation with a representation of the response. @@ -189,6 +228,12 @@ public interface IDeleteItemDocumentRequestBuilder : ITableBuilder WithReturnCollectionMetrics(ReturnItemCollectionMetrics returnItemCollectionMetrics); + /// + /// Suppresses the throwing of the exceptions related to the DeleteItem operation. + /// + /// + ISuppressedDeleteItemDocumentRequestBuilder SuppressThrowing(); + /// /// Executes the DeleteItem operation. /// @@ -214,4 +259,32 @@ public interface IDeleteItemDocumentRequestBuilder : ITableBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } + + public interface ISuppressedDeleteItemDocumentRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the DeleteItem operation and returns the operation result. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the DeleteItem operation and returns the operation result with item's attributes before the delete. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// The item is returned as it appeared before the DeleteItem operation, but only if with was specified in the request chain. + /// Otherwise, null is returned. + /// + Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the DeleteItem operation and returns the operation results with deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/OpResult.cs b/src/EfficientDynamoDb/Operations/OpResult.cs index 667d3e48..03995994 100644 --- a/src/EfficientDynamoDb/Operations/OpResult.cs +++ b/src/EfficientDynamoDb/Operations/OpResult.cs @@ -188,6 +188,12 @@ public static async Task EnsureSuccess(this Task> task) var result = await task.ConfigureAwait(false); return result.EnsureSuccess(); } + + public static async Task EnsureSuccess(this Task task) + { + var result = await task.ConfigureAwait(false); + result.EnsureSuccess(); + } } public enum OpErrorType From 09669e944115d74dab029e78ecb5baff5399f1b0 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 14 Sep 2024 14:49:50 +0200 Subject: [PATCH 07/28] UpdateItem --- .../DynamoDbContext.UpdateItem.cs | 24 ++++--- .../UpdateItem/IUpdateRequestBuilder.cs | 66 +++++++++++++++++ .../UpdateItem/UpdateRequestBuilder.cs | 70 +++++++++++++++++-- 3 files changed, 147 insertions(+), 13 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.UpdateItem.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.UpdateItem.cs index 70db182e..884190b0 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.UpdateItem.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.UpdateItem.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using EfficientDynamoDb.Internal.Metadata; using EfficientDynamoDb.Internal.Operations.UpdateItem; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.UpdateItem; @@ -16,25 +17,32 @@ public partial class DynamoDbContext /// Update operation builder. public IUpdateEntityRequestBuilder UpdateItem() where TEntity : class => new UpdateEntityRequestBuilder(this); - internal async Task> UpdateItemResponseAsync(DdbClassInfo classInfo, BuilderNode? node, + internal async Task>> UpdateItemResponseAsync(DdbClassInfo classInfo, BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new UpdateItemHighLevelHttpContent(this, classInfo, node); + + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - - return await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + return new(result); } - internal async Task UpdateItemAsync(DdbClassInfo classInfo, BuilderNode? node, + internal async Task> UpdateItemAsync(DdbClassInfo classInfo, BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new UpdateItemHighLevelHttpContent(this, classInfo, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); - return result.Item; + return new(result.Item); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/UpdateItem/IUpdateRequestBuilder.cs b/src/EfficientDynamoDb/Operations/UpdateItem/IUpdateRequestBuilder.cs index 83d79277..a86dd40c 100644 --- a/src/EfficientDynamoDb/Operations/UpdateItem/IUpdateRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/UpdateItem/IUpdateRequestBuilder.cs @@ -103,6 +103,11 @@ public interface IUpdateEntityRequestBuilder : IUpdateItemBuilderUpdateItem operation builder suitable for document response. IUpdateDocumentRequestBuilder AsDocument(); + /// + /// Suppresses the throwing of the exceptions related to the UpdateItem operation. + /// + ISuppressedUpdateItemEntityRequestBuilder SuppressThrowing(); + /// /// Executes the UpdateItem operation. /// @@ -128,6 +133,33 @@ public interface IUpdateEntityRequestBuilder : IUpdateItemBuilder> ToResponseAsync(CancellationToken cancellationToken = default); } + public interface ISuppressedUpdateItemEntityRequestBuilder : IUpdateItemBuilder> where TEntity : class + { + /// + /// Executes the UpdateItem operation and returns the operation result. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the UpdateItem operation and returns the operation result with item according to the option set in . + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// If is set to , this method will always return null. + /// + Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the UpdateItem operation and returns the operation results with deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } + /// /// Represents a builder for the UpdateItem operation with an entity type constraint and a document response. /// Provides methods for configuring options and executing the operation with a representation of the response. @@ -211,6 +243,12 @@ public interface IUpdateDocumentRequestBuilder : IUpdateItemBuilder IAttributeUpdate, TEntity, TProperty> On(Expression> expression); + /// + /// Suppresses the throwing of the exceptions related to the UpdateItem operation. + /// + /// + ISuppressedUpdateItemDocumentRequestBuilder SuppressThrowing(); + /// /// Executes the UpdateItem operation. /// @@ -235,4 +273,32 @@ public interface IUpdateDocumentRequestBuilder : IUpdateItemBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } + + public interface ISuppressedUpdateItemDocumentRequestBuilder : IUpdateItemBuilder> + where TEntity : class + { + /// + /// Executes the UpdateItem operation and returns the operation result. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the UpdateItem operation and returns the operation result with item according to the option set in . + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + /// + /// If is set to , this method will always return null. + /// + Task> ToItemAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the UpdateItem operation and returns the operation results with deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/UpdateItem/UpdateRequestBuilder.cs b/src/EfficientDynamoDb/Operations/UpdateItem/UpdateRequestBuilder.cs index 62372357..e917791b 100644 --- a/src/EfficientDynamoDb/Operations/UpdateItem/UpdateRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/UpdateItem/UpdateRequestBuilder.cs @@ -36,13 +36,13 @@ private UpdateEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) public async Task ToItemAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.UpdateItemAsync(classInfo, _node, cancellationToken).ConfigureAwait(false); + return await _context.UpdateItemAsync(classInfo, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.UpdateItemResponseAsync(classInfo, _node, cancellationToken).ConfigureAwait(false); + return await _context.UpdateItemResponseAsync(classInfo, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IAttributeUpdate, TEntity, TProperty> On(Expression> expression) => @@ -73,12 +73,42 @@ public IUpdateEntityRequestBuilder WithPrimaryKey(TPk pk) => new UpdateEntityRequestBuilder(_context, new PartitionKeyNode(pk, _node)); public IUpdateDocumentRequestBuilder AsDocument() => new UpdateDocumentRequestBuilder(_context, _node); + + public ISuppressedUpdateItemEntityRequestBuilder SuppressThrowing() => new SuppressedUpdateEntityRequestBuilder(_context, _node); IUpdateEntityRequestBuilder IUpdateItemBuilder>.Create(UpdateBase update, BuilderNodeType nodeType) => new UpdateEntityRequestBuilder(_context, new UpdateAttributeNode(update, nodeType, _node)); } - internal sealed class UpdateDocumentRequestBuilder : IUpdateDocumentRequestBuilder where TEntity : class + internal sealed class SuppressedUpdateEntityRequestBuilder : ISuppressedUpdateItemEntityRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedUpdateEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + var result = await _context.UpdateItemAsync(classInfo, _node, cancellationToken).ConfigureAwait(false); + return result.DiscardValue(); + } + + public Task> ToItemAsync(CancellationToken cancellationToken = default) => + _context.UpdateItemAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), _node, cancellationToken); + + public Task>> ToResponseAsync(CancellationToken cancellationToken = default) => + _context.UpdateItemResponseAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), _node, cancellationToken); + + ISuppressedUpdateItemEntityRequestBuilder IUpdateItemBuilder>.Create(UpdateBase update, + BuilderNodeType nodeType) => new SuppressedUpdateEntityRequestBuilder(_context, _node); + } + + internal sealed class UpdateDocumentRequestBuilder : IUpdateDocumentRequestBuilder where TEntity : class { private readonly DynamoDbContext _context; private readonly BuilderNode? _node; @@ -104,13 +134,13 @@ internal UpdateDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node public async Task ToItemAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.UpdateItemAsync(classInfo, _node, cancellationToken).ConfigureAwait(false); + return await _context.UpdateItemAsync(classInfo, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); - return await _context.UpdateItemResponseAsync(classInfo, _node, cancellationToken).ConfigureAwait(false); + return await _context.UpdateItemResponseAsync(classInfo, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IAttributeUpdate, TEntity, TProperty> On(Expression> expression) => @@ -136,8 +166,38 @@ public IUpdateDocumentRequestBuilder WithPrimaryKey(TPk pk, T public IUpdateDocumentRequestBuilder WithPrimaryKey(TPk pk) => new UpdateDocumentRequestBuilder(_context, new PartitionKeyNode(pk, _node)); + + public ISuppressedUpdateItemDocumentRequestBuilder SuppressThrowing() => new SuppressedUpdateDocumentRequestBuilder(_context, _node); IUpdateDocumentRequestBuilder IUpdateItemBuilder>.Create(UpdateBase update, BuilderNodeType nodeType) => new UpdateDocumentRequestBuilder(_context, new UpdateAttributeNode(update, nodeType, _node)); } + + internal sealed class SuppressedUpdateDocumentRequestBuilder : ISuppressedUpdateItemDocumentRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedUpdateDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var classInfo = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)); + var result = await _context.UpdateItemAsync(classInfo, _node, cancellationToken).ConfigureAwait(false); + return result.DiscardValue(); + } + + public Task> ToItemAsync(CancellationToken cancellationToken = default) => + _context.UpdateItemAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), _node, cancellationToken); + + public Task>> ToResponseAsync(CancellationToken cancellationToken = default) => + _context.UpdateItemResponseAsync(_context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)), _node, cancellationToken); + + ISuppressedUpdateItemDocumentRequestBuilder IUpdateItemBuilder>.Create(UpdateBase update, + BuilderNodeType nodeType) => new SuppressedUpdateDocumentRequestBuilder(_context, _node); + } } \ No newline at end of file From 6ca4b7d943589dcd14f094491a3e4cab2fb44d76 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 14 Sep 2024 16:35:22 +0200 Subject: [PATCH 08/28] Query --- .../DynamoDbContext/DynamoDbContext.Query.cs | 32 +++-- .../Operations/Query/IQueryRequestBuilder.cs | 114 +++++++++++++++- .../Operations/Query/QueryRequestBuilder.cs | 123 ++++++++++++++++-- 3 files changed, 248 insertions(+), 21 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Query.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Query.cs index cd2e5f75..691cd2c5 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Query.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Query.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using EfficientDynamoDb.Internal.Operations.Query; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.Shared; @@ -17,7 +18,7 @@ public partial class DynamoDbContext /// Query operation builder. public IQueryEntityRequestBuilder Query() where TEntity : class => new QueryEntityRequestBuilder(this); - internal async Task> QueryListAsync(string? tableName, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> QueryListAsync(string? tableName, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { QueryEntityResponseProjection? result = null; List? items = null; @@ -29,7 +30,11 @@ internal async Task> QueryListAsync(string? tabl var contentNode = isFirst ? node : new PaginationTokenNode(result?.PaginationToken, node); using var httpContent = new QueryHighLevelHttpContent(this, tableName, contentNode); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); if (items == null) @@ -40,17 +45,21 @@ internal async Task> QueryListAsync(string? tabl isFirst = false; } while (result.PaginationToken != null); - return items; + return new(items); } - internal async Task> QueryPageAsync(string? tableName, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> QueryPageAsync(string? tableName, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new QueryHighLevelHttpContent(this, tableName, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); - return new PagedResult(result.Items, result.PaginationToken); + return new(new PagedResult(result.Items, result.PaginationToken)); } internal async IAsyncEnumerable> QueryAsyncEnumerable(string? tableName, BuilderNode node, @@ -73,13 +82,18 @@ internal async IAsyncEnumerable> QueryAsyncEnumerable> QueryAsync(string? tableName, BuilderNode node, CancellationToken cancellationToken = default) + internal async Task>> QueryAsync(string? tableName, BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new QueryHighLevelHttpContent(this, tableName, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - return await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + return new (result); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/Query/IQueryRequestBuilder.cs b/src/EfficientDynamoDb/Operations/Query/IQueryRequestBuilder.cs index 46850f24..9f35211d 100644 --- a/src/EfficientDynamoDb/Operations/Query/IQueryRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/Query/IQueryRequestBuilder.cs @@ -183,14 +183,49 @@ public interface IQueryEntityRequestBuilder : ITableBuilder IQueryEntityRequestBuilder WithProjectedAttributes(params Expression>[] properties); + + /// + /// Suppresses throwing of the exceptions related to the Query operation. + /// + /// + ISuppressedQueryEntityRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the Query operation that suppresses DynamoDB-related exceptions. + /// + /// Type of the DB entity. + public interface ISuppressedQueryEntityRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the Query operation and aggregates the results into a single list of items. + /// This method will make at least one service call to DynamoDB and will continue making calls to fetch all available pages of results if necessary. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToListAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Query operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Query operation and returns the page of data with pagination token. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToPageAsync(CancellationToken cancellationToken = default); } - + /// /// Represents a builder for the projected Query operation. /// /// Type of the DB entity. /// Type of the projection. - public interface IQueryEntityRequestBuilder : ITableBuilder> where TEntity : class where TProjection : class + public interface IQueryEntityRequestBuilder : ITableBuilder> where TEntity : class where TProjection : class { /// /// Executes the Query operation and aggregates the results into a single list of items. @@ -315,8 +350,45 @@ public interface IQueryEntityRequestBuilder : ITableBuilde /// Passing null for will result in the same behavior as not specifying the pagination token at all. /// IQueryEntityRequestBuilder WithPaginationToken(string? paginationToken); + + /// + /// Suppresses throwing of the exceptions related to the Query operation. + /// + /// + ISuppressedQueryEntityRequestBuilder SuppressThrowing(); } - + + /// + /// Represents a builder for the projected Query operation that suppresses DynamoDB-related exceptions. + /// + /// Type of the DB entity. + /// Type of the projection. + public interface ISuppressedQueryEntityRequestBuilder : ITableBuilder> + where TEntity : class where TProjection : class + { + /// + /// Executes the Query operation and aggregates the results into a single list of items. + /// This method will make at least one service call to DynamoDB and will continue making calls to fetch all available pages of results if necessary. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToListAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Query operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Query operation and returns the page of data with pagination token. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToPageAsync(CancellationToken cancellationToken = default); + } + /// /// Represents a builder for the Query operation with an entity type constraint and a document response. /// Provides methods for configuring options and executing the operation with a representation of the response. @@ -486,5 +558,41 @@ public interface IQueryDocumentRequestBuilder : ITableBuildernull for will result in the same behavior as not specifying the pagination token at all. /// IQueryDocumentRequestBuilder WithPaginationToken(string? paginationToken); + + /// + /// Suppresses throwing of the exceptions related to the Query operation. + /// + /// + ISuppressedQueryDocumentRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the Query operation that suppresses DynamoDB-related exceptions. + /// Provides methods for executing the operation with a representation of the response. + /// + /// + public interface ISuppressedQueryDocumentRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the Query operation and aggregates the results into a single list of items. + /// This method will make at least one service call to DynamoDB and will continue making calls to fetch all available pages of results if necessary. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToListAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Query operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Query operation and returns the page of data with pagination token. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToPageAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/Query/QueryRequestBuilder.cs b/src/EfficientDynamoDb/Operations/Query/QueryRequestBuilder.cs index da89b6b5..d2e9db76 100644 --- a/src/EfficientDynamoDb/Operations/Query/QueryRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/Query/QueryRequestBuilder.cs @@ -35,7 +35,7 @@ private QueryEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) public async Task> ToListAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async IAsyncEnumerable ToAsyncEnumerable() @@ -59,13 +59,13 @@ public IAsyncEnumerable> ToPagedAsyncEnumerable() public async Task> ToPageAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IQueryDocumentRequestBuilder AsDocuments() => new QueryDocumentRequestBuilder(_context, _node); @@ -110,6 +110,40 @@ public IQueryEntityRequestBuilder AsProjections WithProjectedAttributes(params Expression>[] properties) => new QueryEntityRequestBuilder(_context, new ProjectedAttributesNode(typeof(TEntity), properties, _node)); + + public ISuppressedQueryEntityRequestBuilder SuppressThrowing() => new SuppressedQueryEntityRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty query request."); + } + + internal sealed class SuppressedQueryEntityRequestBuilder : ISuppressedQueryEntityRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedQueryEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToListAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToPageAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty query request."); } @@ -138,7 +172,7 @@ internal QueryEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) public async Task> ToListAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async IAsyncEnumerable ToAsyncEnumerable() @@ -162,13 +196,13 @@ public IAsyncEnumerable> ToPagedAsyncEnumerable() public async Task> ToPageAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IQueryDocumentRequestBuilder AsDocuments() => new QueryDocumentRequestBuilder(_context, _node); @@ -201,8 +235,45 @@ public IQueryEntityRequestBuilder WithFilterExpression(Fil public IQueryEntityRequestBuilder WithFilterExpression(Func, FilterBase> filterSetup) => new QueryEntityRequestBuilder(_context, new FilterExpressionNode(filterSetup(Condition.ForEntity()), _node)); + public IQueryEntityRequestBuilder WithPaginationToken(string? paginationToken) => new QueryEntityRequestBuilder(_context, new PaginationTokenNode(paginationToken, _node)); + + public ISuppressedQueryEntityRequestBuilder SuppressThrowing() => + new SuppressedQueryEntityRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty query request."); + } + + internal sealed class SuppressedQueryEntityRequestBuilder : ISuppressedQueryEntityRequestBuilder + where TEntity : class where TProjection : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedQueryEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToListAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToPageAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty query request."); } @@ -231,7 +302,7 @@ internal QueryDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) public async Task> ToListAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async IAsyncEnumerable ToAsyncEnumerable() @@ -255,13 +326,13 @@ public IAsyncEnumerable> ToPagedAsyncEnumerable() public async Task> ToPageAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.QueryAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.QueryAsync(tableName, GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IQueryDocumentRequestBuilder WithProjectedAttributes() where TProjection : class => @@ -303,7 +374,41 @@ public IQueryDocumentRequestBuilder WithFilterExpression(Func WithPaginationToken(string? paginationToken) => new QueryDocumentRequestBuilder(_context, new PaginationTokenNode(paginationToken, _node)); + public ISuppressedQueryDocumentRequestBuilder SuppressThrowing() => new SuppressedQueryDocumentRequestBuilder(_context, _node); + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty query request."); } + + internal sealed class SuppressedQueryDocumentRequestBuilder : ISuppressedQueryDocumentRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedQueryDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToListAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryListAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToPageAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.QueryPageAsync(tableName, GetNode(), cancellationToken).ConfigureAwait(false); + } + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty query request."); + } } \ No newline at end of file From 2b345f3efa8155fd6e0329f040bbbae0c988b43c Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 14 Sep 2024 17:47:57 +0200 Subject: [PATCH 09/28] Scan --- .../DynamoDbContext/DynamoDbContext.Scan.cs | 23 +++-- .../Operations/Scan/IScanRequestBuilder.cs | 86 +++++++++++++++++ .../Operations/Scan/ScanRequestBuilder.cs | 93 +++++++++++++++++-- 3 files changed, 189 insertions(+), 13 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Scan.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Scan.cs index 4363ae5e..3c6bd1cf 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Scan.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.Scan.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using EfficientDynamoDb.Internal.Operations.Scan; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.Scan; using EfficientDynamoDb.Operations.Shared; @@ -18,14 +19,18 @@ public partial class DynamoDbContext /// Scan operation builder. public IScanEntityRequestBuilder Scan() where TEntity : class => new ScanEntityRequestBuilder(this); - internal async Task> ScanPageAsync(string? tableName, BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> ScanPageAsync(string? tableName, BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new ScanHighLevelHttpContent(this, tableName, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); - return new PagedResult(result.Items, result.PaginationToken); + return new(new PagedResult(result.Items, result.PaginationToken)); } internal async IAsyncEnumerable> ScanAsyncEnumerable(string? tableName, BuilderNode? node, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TEntity : class @@ -50,12 +55,18 @@ internal async IAsyncEnumerable> ScanAsyncEnumerable> ParallelScanAsyncEnumerable(string? tableName, BuilderNode? node, int totalSegments) where TEntity : class => new ParallelScanAsyncEnumerable(this, tableName, node, totalSegments); - internal async Task> ScanAsync(string? tableName, BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> ScanAsync(string? tableName, BuilderNode? node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new ScanHighLevelHttpContent(this, tableName, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - return await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + + return new(result); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/Scan/IScanRequestBuilder.cs b/src/EfficientDynamoDb/Operations/Scan/IScanRequestBuilder.cs index f4a53882..2ec3b70f 100644 --- a/src/EfficientDynamoDb/Operations/Scan/IScanRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/Scan/IScanRequestBuilder.cs @@ -136,6 +136,12 @@ public interface IScanEntityRequestBuilder : ITableBuilderScan operation builder suitable for document response. IScanDocumentRequestBuilder AsDocuments(); + /// + /// Suppresses throwing exceptions for DynamoDB-related errors. + /// + /// + ISuppressedScanEntityRequestBuilder SuppressThrowing(); + /// /// Executes the Scan operation and returns the result as an async enumerable, with each item in the sequence representing a single retrieved item. /// @@ -178,6 +184,29 @@ public interface IScanEntityRequestBuilder : ITableBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } + + /// + /// Represents a builder for the Scan operation that suppresses throwing exceptions for DynamoDB-related errors. + /// + /// + public interface ISuppressedScanEntityRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the Scan operation and returns the page of data with pagination token. + /// This method will not throw an exception if DynamoDB-related error occurs. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToPageAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Scan operation and returns the deserialized response. + /// This method will not throw an exception if DynamoDB-related error occurs. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } /// /// Represents a builder for the projected Scan operation. @@ -261,6 +290,11 @@ public interface IScanEntityRequestBuilder : ITableBuilder /// /// Scan operation builder suitable for document response. IScanDocumentRequestBuilder AsDocuments(); + + /// + /// Suppresses throwing exceptions for DynamoDB-related errors. + /// + ISuppressedScanEntityRequestBuilder SuppressThrowing(); /// /// Executes the Scan operation and returns the result as an async enumerable, with each item in the sequence representing a single retrieved item. @@ -305,6 +339,30 @@ public interface IScanEntityRequestBuilder : ITableBuilder Task> ToResponseAsync(CancellationToken cancellationToken = default); } + /// + /// Represents a builder for the projected Scan operation that suppresses throwing exceptions for DynamoDB-related errors. + /// + /// Type of the DB entity. + /// Type of the projection. + public interface ISuppressedScanEntityRequestBuilder : ITableBuilder> where TEntity : class where TProjection : class + { + /// + /// Executes the Scan operation and returns the page of data with pagination token. + /// This method will not throw an exception if DynamoDB-related error occurs. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToPageAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Scan operation and returns the deserialized response. + /// This method will not throw an exception if DynamoDB-related error occurs. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } + /// /// Represents a builder for the Scan operation with an entity type constraint and a document response. /// Provides methods for configuring options and executing the operation with a representation of the response. @@ -426,6 +484,11 @@ public interface IScanDocumentRequestBuilder : ITableBuildernull for will result in the same behavior as not specifying the pagination token at all. /// IScanDocumentRequestBuilder WithPaginationToken(string? paginationToken); + + /// + /// Suppresses throwing exceptions for DynamoDB-related errors. + /// + ISuppressedScanDocumentRequestBuilder SuppressThrowing(); /// /// Executes the Scan operation and returns the result as an async enumerable, with each item in the sequence representing a single retrieved item. @@ -469,4 +532,27 @@ public interface IScanDocumentRequestBuilder : ITableBuilderA task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); } + + /// + /// Represents a builder for the Scan operation with a document response and suppressed throwing exceptions for DynamoDB-related errors. + /// + /// Type of the DB entity. + public interface ISuppressedScanDocumentRequestBuilder : ITableBuilder> where TEntity : class + { + /// + /// Executes the Scan operation and returns the page of data with pagination token. + /// This method will not throw an exception if DynamoDB-related error occurs. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToPageAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the Scan operation and returns the deserialized response. + /// This method will not throw an exception if DynamoDB-related error occurs. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); + } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/Scan/ScanRequestBuilder.cs b/src/EfficientDynamoDb/Operations/Scan/ScanRequestBuilder.cs index 18384e59..b6256660 100644 --- a/src/EfficientDynamoDb/Operations/Scan/ScanRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/Scan/ScanRequestBuilder.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using EfficientDynamoDb.DocumentModel; using EfficientDynamoDb.FluentCondition.Core; -using EfficientDynamoDb.Internal.Extensions; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.Shared; @@ -71,13 +70,13 @@ public IAsyncEnumerable> ToParallelPagedAsyncEnumerable(i public async Task> ToPageAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.ScanPageAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + return await _context.ScanPageAsync(tableName, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.ScanAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + return await _context.ScanAsync(tableName, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IScanEntityRequestBuilder FromIndex(string indexName) => @@ -116,6 +115,32 @@ public IScanEntityRequestBuilder AsProjections WithProjectedAttributes(params Expression>[] properties) => new ScanEntityRequestBuilder(_context, new ProjectedAttributesNode(typeof(TEntity), properties, _node)); + + public ISuppressedScanEntityRequestBuilder SuppressThrowing() => new SuppressedScanEntityRequestBuilder(_context, _node); + } + + internal sealed class SuppressedScanEntityRequestBuilder : ISuppressedScanEntityRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedScanEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToPageAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.ScanPageAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.ScanAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + } } public class ScanEntityRequestBuilder : IScanEntityRequestBuilder where TEntity : class where TProjection : class @@ -178,13 +203,13 @@ public IAsyncEnumerable> ToParallelPagedAsyncEnumerab public async Task> ToPageAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.ScanPageAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + return await _context.ScanPageAsync(tableName, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.ScanAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + return await _context.ScanAsync(tableName, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IScanEntityRequestBuilder FromIndex(string indexName) => @@ -214,6 +239,34 @@ public IScanEntityRequestBuilder WithPaginationToken(strin new ScanEntityRequestBuilder(_context, new PaginationTokenNode(paginationToken, _node)); public IScanDocumentRequestBuilder AsDocuments() => new ScanDocumentRequestBuilder(_context, _node); + + public ISuppressedScanEntityRequestBuilder SuppressThrowing() => + new SuppressedScanEntityRequestBuilder(_context, _node); + } + + internal sealed class SuppressedScanEntityRequestBuilder : ISuppressedScanEntityRequestBuilder + where TEntity : class where TProjection : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedScanEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToPageAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.ScanPageAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.ScanAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + } } public class ScanDocumentRequestBuilder : IScanDocumentRequestBuilder where TEntity : class @@ -276,13 +329,13 @@ public IAsyncEnumerable> ToParallelPagedAsyncEnumerable( public async Task> ToPageAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.ScanPageAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + return await _context.ScanPageAsync(tableName, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public async Task> ToResponseAsync(CancellationToken cancellationToken = default) { var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; - return await _context.ScanAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + return await _context.ScanAsync(tableName, _node, cancellationToken).EnsureSuccess().ConfigureAwait(false); } public IScanDocumentRequestBuilder FromIndex(string indexName) => @@ -319,5 +372,31 @@ public IScanDocumentRequestBuilder WithFilterExpression(Func WithPaginationToken(string? paginationToken) => new ScanDocumentRequestBuilder(_context, new PaginationTokenNode(paginationToken, _node)); + + public ISuppressedScanDocumentRequestBuilder SuppressThrowing() => new SuppressedScanDocumentRequestBuilder(_context, _node); + } + + internal sealed class SuppressedScanDocumentRequestBuilder : ISuppressedScanDocumentRequestBuilder where TEntity : class + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedScanDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToPageAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.ScanPageAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + } + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + { + var tableName = _context.Config.Metadata.GetOrAddClassInfo(typeof(TEntity)).TableName; + return await _context.ScanAsync(tableName, _node, cancellationToken).ConfigureAwait(false); + } } } \ No newline at end of file From 7d3828d6fbb157e6801ce6c21b219be4bf9fa2d9 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 14 Sep 2024 22:03:56 +0200 Subject: [PATCH 10/28] TransactGetItem --- .../DynamoDbContext.TransactGetItems.cs | 22 ++++-- .../ITransactGetItemsRequestBuilder.cs | 55 ++++++++++++++ .../TransactGetItemsRequestBuilder.cs | 71 ++++++++++++++++--- 3 files changed, 131 insertions(+), 17 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactGetItems.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactGetItems.cs index 6601fa11..8ef32891 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactGetItems.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactGetItems.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using EfficientDynamoDb.Internal.Operations.TransactGetItems; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.TransactGetItems; @@ -15,25 +16,34 @@ public partial class DynamoDbContext /// TransactGet operation builder. public ITransactGetItemsEntityRequestBuilder TransactGet() => new TransactGetItemsEntityRequestBuilder(this); - internal async Task> TransactGetItemsAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> TransactGetItemsAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new TransactGetItemsHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); var entities = new List(result.Responses.Count); foreach (var item in result.Responses) entities.Add(item.Item); - return entities; + return new(entities); } - internal async Task> TransactGetItemsResponseAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> TransactGetItemsResponseAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new TransactGetItemsHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - return await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + return new(result); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/TransactGetItems/ITransactGetItemsRequestBuilder.cs b/src/EfficientDynamoDb/Operations/TransactGetItems/ITransactGetItemsRequestBuilder.cs index 636d667a..7585df92 100644 --- a/src/EfficientDynamoDb/Operations/TransactGetItems/ITransactGetItemsRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/TransactGetItems/ITransactGetItemsRequestBuilder.cs @@ -58,6 +58,33 @@ public interface ITransactGetItemsEntityRequestBuilder /// Token that can be used to cancel the task. /// A task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default) where TResultEntity : class; + + /// + /// Suppresses exceptions from the operation. + /// + ISuppressedTransactGetItemsEntityRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the TransactGet operation that suppresses exceptions. + /// Provides methods for configuring options and executing the operation with a representation of the response. + /// + public interface ISuppressedTransactGetItemsEntityRequestBuilder + { + /// + /// Executes the TransactGet operation and returns the list of entities. + /// + /// Token that can be used to cancel the task. + /// Type of the DB entity. + /// A task that represents the asynchronous operation. + Task>> ToListAsync(CancellationToken cancellationToken = default) where TResultEntity : class; + + /// + /// Executes the TransactGet operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default) where TResultEntity : class; } /// @@ -108,5 +135,33 @@ public interface ITransactGetItemsDocumentRequestBuilder /// Token that can be used to cancel the task. /// A task that represents the asynchronous operation. Task> ToResponseAsync(CancellationToken cancellationToken = default); + + /// + /// Suppresses exceptions from the operation. + /// + ISuppressedTransactGetItemsDocumentRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the TransactGet operation that suppresses exceptions. + /// Provides methods for configuring options and executing the operation with a representation of the response. + /// + public interface ISuppressedTransactGetItemsDocumentRequestBuilder + { + /// + /// Executes the TransactGet operation and returns the list of entities. + /// Every entity is represented as . + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToListAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the TransactGet operation and returns the deserialized response. + /// Every entity in the response is represented as . + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/TransactGetItems/TransactGetItemsRequestBuilder.cs b/src/EfficientDynamoDb/Operations/TransactGetItems/TransactGetItemsRequestBuilder.cs index c66c37c7..6c53ad4d 100644 --- a/src/EfficientDynamoDb/Operations/TransactGetItems/TransactGetItemsRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/TransactGetItems/TransactGetItemsRequestBuilder.cs @@ -29,7 +29,7 @@ public ITransactGetItemsEntityRequestBuilder WithReturnConsumedCapacity(ReturnCo public ITransactGetItemsEntityRequestBuilder WithItems(params ITransactGetItemRequestBuilder[] items) => new TransactGetItemsEntityRequestBuilder(_context, new BatchItemsNode(items, _node)); - + public ITransactGetItemsEntityRequestBuilder WithItems(IEnumerable items) => new TransactGetItemsEntityRequestBuilder(_context, new BatchItemsNode(items, _node)); @@ -37,18 +37,41 @@ public ITransactGetItemsEntityRequestBuilder WithItems(IEnumerable> ToListAsync(CancellationToken cancellationToken = default) where TResultEntity : class { - return await _context.TransactGetItemsAsync(GetNode(), cancellationToken).ConfigureAwait(false); + return await _context.TransactGetItemsAsync(GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); } - - public async Task> ToResponseAsync(CancellationToken cancellationToken = default) where TResultEntity : class + + public async Task> ToResponseAsync(CancellationToken cancellationToken = default) + where TResultEntity : class + { + return await _context.TransactGetItemsResponseAsync(GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); + } + + public ISuppressedTransactGetItemsEntityRequestBuilder SuppressThrowing() => new SuppressedTransactGetItemsEntityRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty transact get items request."); + } + + internal sealed class SuppressedTransactGetItemsEntityRequestBuilder : ISuppressedTransactGetItemsEntityRequestBuilder + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedTransactGetItemsEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) { - return await _context.TransactGetItemsResponseAsync(GetNode(), cancellationToken).ConfigureAwait(false); + _context = context; + _node = node; } + public async Task>> ToListAsync(CancellationToken cancellationToken = default) + where TResultEntity : class => await _context.TransactGetItemsAsync(GetNode(), cancellationToken).ConfigureAwait(false); + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) + where TResultEntity : class => await _context.TransactGetItemsResponseAsync(GetNode(), cancellationToken).ConfigureAwait(false); + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty transact get items request."); } - - internal sealed class TransactGetItemsDocumentRequestBuilder : ITransactGetItemsDocumentRequestBuilder + + internal sealed class TransactGetItemsDocumentRequestBuilder : ITransactGetItemsDocumentRequestBuilder { private readonly DynamoDbContext _context; private readonly BuilderNode? _node; @@ -69,17 +92,43 @@ public ITransactGetItemsDocumentRequestBuilder WithReturnConsumedCapacity(Return public ITransactGetItemsDocumentRequestBuilder WithItems(params ITransactGetItemRequestBuilder[] items) => new TransactGetItemsDocumentRequestBuilder(_context, new BatchItemsNode(items, _node)); - + public ITransactGetItemsDocumentRequestBuilder WithItems(IEnumerable items) => new TransactGetItemsDocumentRequestBuilder(_context, new BatchItemsNode(items, _node)); - - + + public async Task> ToListAsync(CancellationToken cancellationToken = default) + { + return await _context.TransactGetItemsAsync(GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); + } + + public async Task> ToResponseAsync(CancellationToken cancellationToken = default) + { + return await _context.TransactGetItemsResponseAsync(GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); + } + + public ISuppressedTransactGetItemsDocumentRequestBuilder SuppressThrowing() => new SuppressedTransactGetItemsDocumentRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty transact get items request."); + } + + internal sealed class SuppressedTransactGetItemsDocumentRequestBuilder : ISuppressedTransactGetItemsDocumentRequestBuilder + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedTransactGetItemsDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToListAsync(CancellationToken cancellationToken = default) { return await _context.TransactGetItemsAsync(GetNode(), cancellationToken).ConfigureAwait(false); } - public async Task> ToResponseAsync(CancellationToken cancellationToken = default) + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) { return await _context.TransactGetItemsResponseAsync(GetNode(), cancellationToken).ConfigureAwait(false); } From f5eb7b912ad6d86f71f7d057ee7bf0af80d2dd48 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 14 Sep 2024 22:09:10 +0200 Subject: [PATCH 11/28] TransactWriteItem --- .../DynamoDbContext.TransactWriteItems.cs | 24 +++++++++++++----- .../ITransactWriteItemsRequestBuilder.cs | 25 +++++++++++++++++++ .../TransactWriteItemsRequestBuilder.cs | 24 ++++++++++++++++-- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactWriteItems.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactWriteItems.cs index 56d005cb..e7d47259 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactWriteItems.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.TransactWriteItems.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using EfficientDynamoDb.Internal.Operations.TransactWriteItems; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.TransactWriteItems; using EfficientDynamoDb.Operations.TransactWriteItems.Builders; @@ -15,20 +16,31 @@ public partial class DynamoDbContext /// TransactWrite operation builder. public ITransactWriteItemsRequestBuilder TransactWrite() => new TransactWriteItemsRequestBuilder(this); - internal async Task TransactWriteItemsAsync(BuilderNode node, CancellationToken cancellationToken = default) + internal async Task TransactWriteItemsAsync(BuilderNode node, CancellationToken cancellationToken = default) { using var httpContent = new TransactWriteItemsHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; await ReadAsync(response, cancellationToken).ConfigureAwait(false); + return new(); } - - internal async Task TransactWriteItemsResponseAsync(BuilderNode node, CancellationToken cancellationToken = default) + + internal async Task> TransactWriteItemsResponseAsync(BuilderNode node, + CancellationToken cancellationToken = default) { using var httpContent = new TransactWriteItemsHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - return await ReadAsync(response, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; + var result = await ReadAsync(response, cancellationToken).ConfigureAwait(false); + return new(result); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/ITransactWriteItemsRequestBuilder.cs b/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/ITransactWriteItemsRequestBuilder.cs index bf26e011..c3f66af0 100644 --- a/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/ITransactWriteItemsRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/ITransactWriteItemsRequestBuilder.cs @@ -59,5 +59,30 @@ public interface ITransactWriteItemsRequestBuilder /// Token that can be used to cancel the task. /// A task that represents the asynchronous operation. Task ToResponseAsync(CancellationToken cancellationToken = default); + + /// + /// Suppresses exceptions from the operation. + /// + ISuppressedTransactWriteItemsRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the TransactWrite operation that suppresses exceptions. + /// + public interface ISuppressedTransactWriteItemsRequestBuilder + { + /// + /// Executes the TransactWrite operation. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the TransactWrite operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task> ToResponseAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/TransactWriteItemsRequestBuilder.cs b/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/TransactWriteItemsRequestBuilder.cs index 882ceabb..9d87f54c 100644 --- a/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/TransactWriteItemsRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/TransactWriteItems/Builders/TransactWriteItemsRequestBuilder.cs @@ -38,12 +38,32 @@ public ITransactWriteItemsRequestBuilder WithItems(params ITransactWriteItemBuil public ITransactWriteItemsRequestBuilder WithItems(IEnumerable items) => new TransactWriteItemsRequestBuilder(_context, new BatchItemsNode(items, _node)); - public Task ExecuteAsync(CancellationToken cancellationToken = default) => _context.TransactWriteItemsAsync(GetNode(), cancellationToken); + public Task ExecuteAsync(CancellationToken cancellationToken = default) => _context.TransactWriteItemsAsync(GetNode(), cancellationToken).EnsureSuccess(); public Task ToResponseAsync(CancellationToken cancellationToken = default) => + _context.TransactWriteItemsResponseAsync(GetNode(), cancellationToken).EnsureSuccess(); + + public ISuppressedTransactWriteItemsRequestBuilder SuppressThrowing() => new SuppressedTransactWriteItemsRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty transact write items request."); + } + + internal sealed class SuppressedTransactWriteItemsRequestBuilder : ISuppressedTransactWriteItemsRequestBuilder + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + internal SuppressedTransactWriteItemsRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public Task ExecuteAsync(CancellationToken cancellationToken = default) => _context.TransactWriteItemsAsync(GetNode(), cancellationToken); + + public Task> ToResponseAsync(CancellationToken cancellationToken = default) => _context.TransactWriteItemsResponseAsync(GetNode(), cancellationToken); - private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty transact write items request."); } } \ No newline at end of file From 319f8d33e31ee97b5e4c9b343422fa56bf45a1db Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sun, 15 Sep 2024 15:08:26 +0200 Subject: [PATCH 12/28] BatchWriteItem --- .../DynamoDbContext,BatchWriteItem.cs | 39 ++++++++++++++----- .../BatchWriteItemRequestBuilder.cs | 28 ++++++++++++- .../IBatchWriteItemRequestBuilder.cs | 26 +++++++++++++ 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext,BatchWriteItem.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext,BatchWriteItem.cs index 11f88022..78ad93b2 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext,BatchWriteItem.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext,BatchWriteItem.cs @@ -5,6 +5,7 @@ using EfficientDynamoDb.Exceptions; using EfficientDynamoDb.Internal.Operations.BatchWriteItem; using EfficientDynamoDb.Internal.Operations.Shared; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.BatchWriteItem; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.Shared; @@ -20,11 +21,15 @@ public partial class DynamoDbContext /// BatchWrite operation builder. public IBatchWriteItemRequestBuilder BatchWrite() => new BatchWriteItemRequestBuilder(this); - internal async Task BatchWriteItemAsync(BuilderNode node, CancellationToken cancellationToken = default) + internal async Task BatchWriteItemAsync(BuilderNode node, CancellationToken cancellationToken = default) { using var httpContent = new BatchWriteItemHighLevelHttpContent(this, node, Config.TableNamePrefix); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var documentResult = await DynamoDbLowLevelContext.ReadDocumentAsync(response, BatchWriteItemParsingOptions.Instance, cancellationToken).ConfigureAwait(false); var attempt = 0; @@ -35,24 +40,34 @@ internal async Task BatchWriteItemAsync(BuilderNode node, CancellationToken canc break; if (!Config.RetryStrategies.ProvisionedThroughputExceededStrategy.TryGetRetryDelay(attempt++, out var delay)) - throw new DdbException($"Maximum number of {attempt} attempts exceeded while executing batch write item request."); + return new(new DdbException($"Maximum number of {attempt} attempts exceeded while executing batch write item request.")); await Task.Delay(delay, cancellationToken).ConfigureAwait(false); using var unprocessedHttpContent = new BatchWriteItemHttpContent(new BatchWriteItemRequest{RequestItems = unprocessedItems}, null); - using var unprocessedResponse = await Api.SendAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); + var unprocessedApiResult = await Api.SendSafeAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); + if (unprocessedApiResult.Exception is not null) + return new(unprocessedApiResult.Exception); + + using var unprocessedResponse = unprocessedApiResult.Response!; documentResult = await DynamoDbLowLevelContext.ReadDocumentAsync(unprocessedResponse, BatchWriteItemParsingOptions.Instance, cancellationToken).ConfigureAwait(false); } + + return new(); } - internal async Task BatchWriteItemResponseAsync(BuilderNode node, CancellationToken cancellationToken = default) + internal async Task> BatchWriteItemResponseAsync(BuilderNode node, CancellationToken cancellationToken = default) { using var httpContent = new BatchWriteItemHighLevelHttpContent(this, node, Config.TableNamePrefix); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var documentResult = await DynamoDbLowLevelContext.ReadDocumentAsync(response, BatchWriteItemParsingOptions.Instance, cancellationToken).ConfigureAwait(false); if (documentResult == null) - return new BatchWriteItemResponse(null, null, null); + return new(new BatchWriteItemResponse(null, null, null)); var unprocessedItems = BatchWriteItemResponseParser.ParseFailedItems(documentResult); var consumedCapacity = CapacityParser.ParseFullConsumedCapacities(documentResult); @@ -62,12 +77,16 @@ internal async Task BatchWriteItemResponseAsync(BuilderN while (unprocessedItems != null && unprocessedItems.Count > 0) { if (!Config.RetryStrategies.ProvisionedThroughputExceededStrategy.TryGetRetryDelay(attempt++, out var delay)) - throw new DdbException($"Maximum number of {attempt} attempts exceeded while executing batch write item request."); + return new(new DdbException($"Maximum number of {attempt} attempts exceeded while executing batch write item request.")); await Task.Delay(delay, cancellationToken).ConfigureAwait(false); using var unprocessedHttpContent = new BatchWriteItemHttpContent(new BatchWriteItemRequest{RequestItems = unprocessedItems}, null); - using var unprocessedResponse = await Api.SendAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); + var unprocessedApiResult = await Api.SendSafeAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); + if (unprocessedApiResult.Exception is not null) + return new(unprocessedApiResult.Exception); + + using var unprocessedResponse = unprocessedApiResult.Response!; documentResult = await DynamoDbLowLevelContext.ReadDocumentAsync(unprocessedResponse, BatchWriteItemParsingOptions.Instance, cancellationToken).ConfigureAwait(false); if (documentResult == null) break; @@ -77,7 +96,7 @@ internal async Task BatchWriteItemResponseAsync(BuilderN MergeItemCollectionMetrics(ref itemCollectionMetrics, ItemCollectionMetricsParser.ParseMultipleItemCollectionMetrics(documentResult)); } - return new BatchWriteItemResponse(consumedCapacity, itemCollectionMetrics, unprocessedItems); + return new(new BatchWriteItemResponse(consumedCapacity, itemCollectionMetrics, unprocessedItems)); } private static void MergeFullCapacities(ref List? total, List? current) diff --git a/src/EfficientDynamoDb/Operations/BatchWriteItem/BatchWriteItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/BatchWriteItem/BatchWriteItemRequestBuilder.cs index 0bebee1a..3b620555 100644 --- a/src/EfficientDynamoDb/Operations/BatchWriteItem/BatchWriteItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/BatchWriteItem/BatchWriteItemRequestBuilder.cs @@ -36,9 +36,33 @@ public IBatchWriteItemRequestBuilder WithReturnItemCollectionMetrics(ReturnItemC new BatchWriteItemRequestBuilder(_context, new ReturnItemCollectionMetricsNode(returnItemCollectionMetrics, _node)); public Task ExecuteAsync(CancellationToken cancellationToken = default) => - _context.BatchWriteItemAsync(_node ?? throw new DdbException("Can't execute empty batch write item request."), cancellationToken); + _context.BatchWriteItemAsync(GetNode(), cancellationToken).EnsureSuccess(); public Task ToResponseAsync(CancellationToken cancellationToken = default) => - _context.BatchWriteItemResponseAsync(_node ?? throw new DdbException("Can't execute empty batch write item request."), cancellationToken); + _context.BatchWriteItemResponseAsync(GetNode(), cancellationToken).EnsureSuccess(); + + public ISuppressedBatchWriteItemRequestBuilder SuppressThrowing() => new SuppressedBatchWriteItemRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch write item request."); + } + + internal sealed class SuppressedBatchWriteItemRequestBuilder : ISuppressedBatchWriteItemRequestBuilder + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + public SuppressedBatchWriteItemRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public Task ExecuteAsync(CancellationToken cancellationToken = default) => + _context.BatchWriteItemAsync(GetNode(), cancellationToken); + + public Task> ToResponseAsync(CancellationToken cancellationToken = default) => + _context.BatchWriteItemResponseAsync(GetNode(), cancellationToken); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch write item request."); } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/BatchWriteItem/IBatchWriteItemRequestBuilder.cs b/src/EfficientDynamoDb/Operations/BatchWriteItem/IBatchWriteItemRequestBuilder.cs index 5f41131c..9364c13e 100644 --- a/src/EfficientDynamoDb/Operations/BatchWriteItem/IBatchWriteItemRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/BatchWriteItem/IBatchWriteItemRequestBuilder.cs @@ -60,5 +60,31 @@ public interface IBatchWriteItemRequestBuilder /// The option. /// BatchWrite operation builder. IBatchWriteItemRequestBuilder WithReturnItemCollectionMetrics(ReturnItemCollectionMetrics returnItemCollectionMetrics); + + /// + /// Suppresses DynamoDB exceptions. + /// + /// BatchWrite operation builder. + ISuppressedBatchWriteItemRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the BatchWrite operation with suppressed DynamoDB exceptions. + /// + public interface ISuppressedBatchWriteItemRequestBuilder + { + /// + /// Executes the BatchWrite operation. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task ExecuteAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the BatchWrite operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task> ToResponseAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file From cb0f65fdc1e0ecc92fff85db5e12725e1a862d1c Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sun, 15 Sep 2024 15:35:23 +0200 Subject: [PATCH 13/28] BatchGetItem --- .../DynamoDbContext.BatchGetItem.cs | 55 +++++++++++++------ .../BatchGetItem/BatchGetRequestBuilder.cs | 50 ++++++++++++++++- .../BatchGetItem/IBatchGetRequestBuilder.cs | 55 +++++++++++++++++++ 3 files changed, 140 insertions(+), 20 deletions(-) diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.BatchGetItem.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.BatchGetItem.cs index ee445a01..3d5f98cc 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.BatchGetItem.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbContext.BatchGetItem.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using EfficientDynamoDb.Exceptions; using EfficientDynamoDb.Internal.Operations.BatchGetItem; +using EfficientDynamoDb.Operations; using EfficientDynamoDb.Operations.BatchGetItem; using EfficientDynamoDb.Operations.Query; using EfficientDynamoDb.Operations.Shared.Capacity; @@ -18,11 +19,15 @@ public partial class DynamoDbContext /// BatchGet operation builder. public IBatchGetEntityRequestBuilder BatchGet() => new BatchGetEntityRequestBuilder(this); - internal async Task> BatchGetItemListAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> BatchGetItemListAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new BatchGetItemHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); List? items = null; @@ -32,47 +37,63 @@ internal async Task> BatchGetItemListAsync(BuilderNode no while (result.UnprocessedKeys?.Count > 0) { if (!Config.RetryStrategies.ProvisionedThroughputExceededStrategy.TryGetRetryDelay(attempt++, out var delay)) - throw new DdbException($"Maximum number of {attempt} attempts exceeded while executing batch read item request."); + return new(new DdbException($"Maximum number of {attempt} attempts exceeded while executing batch read item request.")); await Task.Delay(delay, cancellationToken).ConfigureAwait(false); using var unprocessedHttpContent = new BatchGetItemHttpContent(new BatchGetItemRequest {RequestItems = result.UnprocessedKeys}, null); - using var unprocessedResponse = await Api.SendAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); + var unprocessedApiResult = await Api.SendSafeAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); + if (unprocessedApiResult.Exception is not null) + return new(unprocessedApiResult.Exception); + + using var unprocessedResponse = unprocessedApiResult.Response!; result = await ReadAsync>(unprocessedResponse, cancellationToken).ConfigureAwait(false); ExtractItems(ref items, result.Responses); } - return items ?? new List(); + return new(items ?? new List()); } - internal async Task> BatchGetItemResponseAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class + internal async Task>> BatchGetItemResponseAsync(BuilderNode node, CancellationToken cancellationToken = default) where TEntity : class { using var httpContent = new BatchGetItemHighLevelHttpContent(this, node); - using var response = await Api.SendAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); - var apiResult = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); + var apiResult = await Api.SendSafeAsync(Config, httpContent, cancellationToken).ConfigureAwait(false); + if (apiResult.Exception is not null) + return new(apiResult.Exception); + + using var response = apiResult.Response!; + var result = await ReadAsync>(response, cancellationToken).ConfigureAwait(false); List? items = null; - ExtractItems(ref items, apiResult.Responses); - var totalConsumedCapacity = apiResult.ConsumedCapacity; + ExtractItems(ref items, result.Responses); + var totalConsumedCapacity = result.ConsumedCapacity; var attempt = 0; - while (apiResult.UnprocessedKeys?.Count > 0) + while (result.UnprocessedKeys?.Count > 0) { if (!Config.RetryStrategies.ProvisionedThroughputExceededStrategy.TryGetRetryDelay(attempt++, out var delay)) break; await Task.Delay(delay, cancellationToken).ConfigureAwait(false); - using var unprocessedHttpContent = new BatchGetItemHttpContent(new BatchGetItemRequest {RequestItems = apiResult.UnprocessedKeys}, null); + using var unprocessedHttpContent = new BatchGetItemHttpContent(new BatchGetItemRequest {RequestItems = result.UnprocessedKeys}, null); - using var unprocessedResponse = await Api.SendAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); - apiResult = await ReadAsync>(unprocessedResponse, cancellationToken).ConfigureAwait(false); + var unprocessedApiResult = await Api.SendSafeAsync(Config, unprocessedHttpContent, cancellationToken).ConfigureAwait(false); + if (unprocessedApiResult.Exception is not null) + return new(unprocessedApiResult.Exception); - ExtractItems(ref items, apiResult.Responses); - MergeTableCapacities(ref totalConsumedCapacity, apiResult.ConsumedCapacity); + using var unprocessedResponse = unprocessedApiResult.Response!; + result = await ReadAsync>(unprocessedResponse, cancellationToken).ConfigureAwait(false); + + ExtractItems(ref items, result.Responses); + MergeTableCapacities(ref totalConsumedCapacity, result.ConsumedCapacity); } - return new BatchGetItemResponse(totalConsumedCapacity, items ?? (IReadOnlyList)Array.Empty(), apiResult.UnprocessedKeys); + return new(new BatchGetItemResponse( + totalConsumedCapacity, + items ?? (IReadOnlyList)Array.Empty(), + result.UnprocessedKeys) + ); } private static void ExtractItems(ref List? items, IReadOnlyDictionary>? responses) diff --git a/src/EfficientDynamoDb/Operations/BatchGetItem/BatchGetRequestBuilder.cs b/src/EfficientDynamoDb/Operations/BatchGetItem/BatchGetRequestBuilder.cs index afae6b16..fb061c0b 100644 --- a/src/EfficientDynamoDb/Operations/BatchGetItem/BatchGetRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/BatchGetItem/BatchGetRequestBuilder.cs @@ -42,9 +42,31 @@ public IBatchGetEntityRequestBuilder WithReturnConsumedCapacity(ReturnConsumedCa public IBatchGetDocumentRequestBuilder AsDocuments() => new BatchGetDocumentRequestBuilder(_context, _node); public async Task> ToListAsync(CancellationToken cancellationToken = default) where TEntity : class => - await _context.BatchGetItemListAsync(GetNode(), cancellationToken).ConfigureAwait(false); + await _context.BatchGetItemListAsync(GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); public async Task> ToResponseAsync(CancellationToken cancellationToken = default) where TEntity : class => + await _context.BatchGetItemResponseAsync(GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); + + public ISuppressedBatchGetEntityRequestBuilder SuppressThrowing() => new SuppressedBatchGetEntityRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch get item request."); + } + + internal sealed class SuppressedBatchGetEntityRequestBuilder : ISuppressedBatchGetEntityRequestBuilder + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + public SuppressedBatchGetEntityRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToListAsync(CancellationToken cancellationToken = default) where TEntity : class => + await _context.BatchGetItemListAsync(GetNode(), cancellationToken).ConfigureAwait(false); + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) where TEntity : class => await _context.BatchGetItemResponseAsync(GetNode(), cancellationToken).ConfigureAwait(false); private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch get item request."); @@ -82,10 +104,32 @@ public IBatchGetDocumentRequestBuilder WithReturnConsumedCapacity(ReturnConsumed new BatchGetDocumentRequestBuilder(_context, new ReturnConsumedCapacityNode(returnConsumedCapacity, _node)); public async Task> ToListAsync(CancellationToken cancellationToken = default) => - await _context.BatchGetItemListAsync(GetNode(), cancellationToken).ConfigureAwait(false); + await _context.BatchGetItemListAsync(GetNode(), cancellationToken).EnsureSuccess().ConfigureAwait(false); public Task> ToResponseAsync(CancellationToken cancellationToken = default) => - _context.BatchGetItemResponseAsync(GetNode(), cancellationToken); + _context.BatchGetItemResponseAsync(GetNode(), cancellationToken).EnsureSuccess(); + + public ISuppressedBatchGetDocumentRequestBuilder SuppressThrowing() => new SuppressedBatchGetDocumentRequestBuilder(_context, _node); + + private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch get item request."); + } + + internal sealed class SuppressedBatchGetDocumentRequestBuilder : ISuppressedBatchGetDocumentRequestBuilder + { + private readonly DynamoDbContext _context; + private readonly BuilderNode? _node; + + public SuppressedBatchGetDocumentRequestBuilder(DynamoDbContext context, BuilderNode? node) + { + _context = context; + _node = node; + } + + public async Task>> ToListAsync(CancellationToken cancellationToken = default) => + await _context.BatchGetItemListAsync(GetNode(), cancellationToken).ConfigureAwait(false); + + public async Task>> ToResponseAsync(CancellationToken cancellationToken = default) => + await _context.BatchGetItemResponseAsync(GetNode(), cancellationToken).ConfigureAwait(false); private BuilderNode GetNode() => _node ?? throw new DdbException("Can't execute empty batch get item request."); } diff --git a/src/EfficientDynamoDb/Operations/BatchGetItem/IBatchGetRequestBuilder.cs b/src/EfficientDynamoDb/Operations/BatchGetItem/IBatchGetRequestBuilder.cs index 324b50df..2207eb97 100644 --- a/src/EfficientDynamoDb/Operations/BatchGetItem/IBatchGetRequestBuilder.cs +++ b/src/EfficientDynamoDb/Operations/BatchGetItem/IBatchGetRequestBuilder.cs @@ -75,6 +75,34 @@ public interface IBatchGetEntityRequestBuilder /// /// BatchGet operation builder suitable for document response. IBatchGetDocumentRequestBuilder AsDocuments(); + + /// + /// Suppresses DynamoDB exceptions. + /// + /// BatchGet operation builder. + ISuppressedBatchGetEntityRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the BatchGet operation with suppressed DynamoDB exceptions. + /// + public interface ISuppressedBatchGetEntityRequestBuilder + { + /// + /// Executes the BatchGet operation and returns the list of entities. + /// + /// Token that can be used to cancel the task. + /// Type of the DB entity. + /// A task that represents the asynchronous operation. + Task>> ToListAsync(CancellationToken cancellationToken = default) where TEntity : class; + + /// + /// Executes the BatchGet operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// Type of the DB entity. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default) where TEntity : class; } /// @@ -141,5 +169,32 @@ public interface IBatchGetDocumentRequestBuilder /// The option. /// BatchGet operation builder. IBatchGetDocumentRequestBuilder WithReturnConsumedCapacity(ReturnConsumedCapacity returnConsumedCapacity); + + /// + /// Suppresses DynamoDB exceptions. + /// + /// BatchGet operation builder. + public ISuppressedBatchGetDocumentRequestBuilder SuppressThrowing(); + } + + /// + /// Represents a builder for the BatchGet operation with document response and suppressed DynamoDB exceptions. + /// + public interface ISuppressedBatchGetDocumentRequestBuilder + { + /// + /// Executes the BatchGet operation and returns the list of entities. + /// Every entity is represented as . + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToListAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the BatchGet operation and returns the deserialized response. + /// + /// Token that can be used to cancel the task. + /// A task that represents the asynchronous operation. + Task>> ToResponseAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file From 1c27aeeba37601b38a3099eea384bf5bc8e0c22c Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sun, 15 Sep 2024 15:35:33 +0200 Subject: [PATCH 14/28] Fix parallel scan --- .../Internal/Operations/Scan/ParallelScanAsyncEnumerator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/EfficientDynamoDb/Internal/Operations/Scan/ParallelScanAsyncEnumerator.cs b/src/EfficientDynamoDb/Internal/Operations/Scan/ParallelScanAsyncEnumerator.cs index c48a431f..2a69046b 100644 --- a/src/EfficientDynamoDb/Internal/Operations/Scan/ParallelScanAsyncEnumerator.cs +++ b/src/EfficientDynamoDb/Internal/Operations/Scan/ParallelScanAsyncEnumerator.cs @@ -83,7 +83,10 @@ public async ValueTask MoveNextAsync() CancellationToken cancellationToken = default) { var result = await _asyncEnumerable.Context.ScanPageAsync(tableName, new SegmentNode(segment, node), cancellationToken).ConfigureAwait(false); - return (segment, result); + if (result.Exception is not null) + throw result.Exception; + + return (segment, result.Value!); } } } \ No newline at end of file From 1b138bdee9183cf0a0cc7bc225c8751a55a77ad9 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 20:20:39 +0200 Subject: [PATCH 15/28] Use latest lang version --- src/EfficientDynamoDb/EfficientDynamoDb.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EfficientDynamoDb/EfficientDynamoDb.csproj b/src/EfficientDynamoDb/EfficientDynamoDb.csproj index ba49858f..65bd40bc 100644 --- a/src/EfficientDynamoDb/EfficientDynamoDb.csproj +++ b/src/EfficientDynamoDb/EfficientDynamoDb.csproj @@ -15,7 +15,7 @@ true true snupkg - 9 + latest From 2d5d57ef6f76c041a790d79c33bf328c0cd3f43a Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 20:42:17 +0200 Subject: [PATCH 16/28] GetItem integration tests --- .../DataPlane/GetItem/GetItemShould.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/GetItem/GetItemShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/GetItem/GetItemShould.cs index c6c60710..d918a2d8 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/GetItem/GetItemShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/GetItem/GetItemShould.cs @@ -1,3 +1,4 @@ +using EfficientDynamoDb.Exceptions; using EfficientDynamoDb.Internal.Extensions; using EfficientDynamoDb.Operations.Shared; using NUnit.Framework; @@ -51,6 +52,18 @@ public async Task ReturnItemWhenItemExists() result.ShouldBe(_testUser); } + [Test] + public void ThrowsExceptionWhenMissingSortKey() + { + // Pass only partition key when both partition key and sort key are required + // This should trigger a validation error + Should.Throw(() => + _context.GetItem() + .WithPrimaryKey(_testUser.PartitionKey) + .ToItemAsync() + ); + } + [Test] public async Task ReturnItemUsingBuilder() { @@ -150,4 +163,32 @@ public async Task ReturnResponseWithMetadata(bool useConsistentRead) response.ConsumedCapacity.ShouldNotBeNull(); response.ConsumedCapacity.CapacityUnits.ShouldBe(expectedConsumedCapacity); } + + [Test] + public async Task ReturnSuccessfulResultWhenItemExistsAndSuppressedThrowing() + { + var result = await _context.GetItem() + .WithPrimaryKey(_testUser.PartitionKey, _testUser.SortKey) + .SuppressThrowing() + .ToItemAsync(); + + result.IsSuccess.ShouldBeTrue(); + result.Exception.ShouldBeNull(); + result.Value.ShouldBe(_testUser); + } + + [Test] + public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() + { + // Pass only partition key when both partition key and sort key are required + // This should trigger a validation error + var result = await _context.GetItem() + .WithPrimaryKey(_testUser.PartitionKey) + .SuppressThrowing() + .ToItemAsync(); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + result.Value.ShouldBeNull(); + } } \ No newline at end of file From 7ed2c6d76b728eb515da703efd6af5c719c77eb7 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 21:10:25 +0200 Subject: [PATCH 17/28] PutItem integration tests --- .../DataPlane/PutItem/PutItemShould.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/PutItem/PutItemShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/PutItem/PutItemShould.cs index aee9d4bd..f37f1020 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/PutItem/PutItemShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/PutItem/PutItemShould.cs @@ -51,6 +51,20 @@ public async Task CreateNewItemSuccessfully() retrievedItem.ShouldBe(testUser); } + [Test] + public void ThrowWhenMissingSortKey() + { + Should.Throw(async () => + { + var testUser = new TestUser { PartitionKey = $"{KeyPrefix}-missing-sort-key-pk", SortKey = "", Name = "test", Age = 25, Email = "test@example.com" }; + await _context.PutItem() + .WithItem(testUser) + .ExecuteAsync(); + _testPartitionKey = testUser.PartitionKey; + _testSortKey = testUser.SortKey; + }); + } + [Test] public async Task ReplaceExistingItemSuccessfully() { @@ -380,4 +394,47 @@ await _context.PutItem() .ToItemAsync(); retrievedItem.ShouldBe(testUser); } + + [Test] + public async Task CreateNewItemWhenSuppressedThrowing() + { + _testPartitionKey = $"{KeyPrefix}-suppress_error_success-pk"; + _testSortKey = $"{KeyPrefix}-suppress_error_success-sk"; + var testUser = new TestUser + { + PartitionKey = _testPartitionKey, + SortKey = _testSortKey, + Name = "Suppress Error User", + Age = 25, + Email = "suppress_error@example.com" + }; + + var result = await _context.PutItem() + .WithItem(testUser) + .SuppressThrowing() + .ExecuteAsync(); + + result.IsSuccess.ShouldBeTrue(); + } + + [Test] + public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() + { + var testUser = new TestUser + { + PartitionKey = $"{KeyPrefix}-suppress_error_fail-pk", + SortKey = "", // Empty sort key - this should trigger a validation error + Name = "Suppress Error User", + Age = 25, + Email = "suppress_error@example.com" + }; + + var result = await _context.PutItem() + .WithItem(testUser) + .SuppressThrowing() + .ExecuteAsync(); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + } } \ No newline at end of file From 2fca785241cda47ce3d7009505ddd54fce891a3b Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 21:13:41 +0200 Subject: [PATCH 18/28] UpdateItem integration tests --- .../DataPlane/UpdateItem/UpdateItemShould.cs | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/UpdateItem/UpdateItemShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/UpdateItem/UpdateItemShould.cs index 94586f18..f905fcaf 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/UpdateItem/UpdateItemShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/UpdateItem/UpdateItemShould.cs @@ -577,7 +577,7 @@ public async Task ReturnDocumentWhenRequested() .WithReturnValues(ReturnValues.AllNew) .AsDocument() .ToItemAsync(); - + document.ShouldNotBeNull(); var updatedUser = document.ToObject(_context.Config.Metadata); updatedUser.ShouldNotBeNull(); @@ -586,4 +586,55 @@ public async Task ReturnDocumentWhenRequested() updatedUser.Name.ShouldBe(testUser.Name); updatedUser.Age.ShouldBe(30); } + + [Test] + public void ThrowWhenMissingSortKey() + { + Should.Throw(async () => + { + await _context.UpdateItem() + .WithPrimaryKey($"{KeyPrefix}-missing-sort-key-pk", "") + .On(x => x.Name).Assign("test") + .ExecuteAsync(); + }); + } + + [Test] + public async Task UpdateNewItemWhenSuppressedThrowing() + { + _testPartitionKey = $"{KeyPrefix}-suppress_error_success-pk"; + _testSortKey = $"{KeyPrefix}-suppress_error_success-sk"; + + // Create initial item + var testUser = new TestUser + { + PartitionKey = _testPartitionKey, + SortKey = _testSortKey, + Name = "Suppress Error User", + Age = 25, + Email = "suppress_error@example.com" + }; + await _context.PutItemAsync(testUser); + + var result = await _context.UpdateItem() + .WithPrimaryKey(_testPartitionKey, _testSortKey) + .On(x => x.Name).Assign("Updated Suppress Error User") + .SuppressThrowing() + .ExecuteAsync(); + + result.IsSuccess.ShouldBeTrue(); + } + + [Test] + public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() + { + var result = await _context.UpdateItem() + .WithPrimaryKey($"{KeyPrefix}-suppress_error_fail-pk", "") // Empty sort key - this should trigger a validation error + .On(x => x.Name).Assign("test") + .SuppressThrowing() + .ExecuteAsync(); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + } } \ No newline at end of file From 890719ec1489a59f2e52f494f9545faa7c5083f4 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 21:30:07 +0200 Subject: [PATCH 19/28] Scan integration tests --- .../DataPlane/Scan/ScanShould.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs index 50d73526..d7955c2c 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs @@ -1,3 +1,5 @@ +using EfficientDynamoDb.Exceptions; +using EfficientDynamoDb.Extensions; using EfficientDynamoDb.Operations.Shared; using NUnit.Framework; using Shouldly; @@ -324,4 +326,43 @@ public async Task ScanWithStringContainsFilterSuccessfully() scannedItems.ShouldAllBe(x => x.Name.Contains("Alice")); } + + [Test] + public void ThrowWhenInvalidScanParameters() + { + Should.Throw(async () => + { + await _context.Scan() + .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithTableName("non_existent_table") + .ToAsyncEnumerable() + .ToListAsync(); + }); + } + + [Test] + public async Task ScanItemsWhenSuppressedThrowing() + { + var result = await _context.Scan() + .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithLimit(2) + .SuppressThrowing() + .ToPageAsync(); + + result.IsSuccess.ShouldBeTrue(); + result.Value.Items.Count.ShouldBeLessThanOrEqualTo(2); + } + + [Test] + public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() + { + var result = await _context.Scan() + .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithTableName("non_existent_table") + .SuppressThrowing() + .ToPageAsync(); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + } } \ No newline at end of file From 2f9cc8593d49926ab8cc1ecc4382f637330ac919 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 21:32:06 +0200 Subject: [PATCH 20/28] Query integration tests --- .../DataPlane/Query/QueryShould.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs index 5eeefe82..910707ef 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs @@ -1,3 +1,5 @@ +using EfficientDynamoDb.Exceptions; +using EfficientDynamoDb.Extensions; using EfficientDynamoDb.Operations.Shared; using NUnit.Framework; using Shouldly; @@ -436,4 +438,43 @@ public async Task QueryItemsWithSortKeyLessThanOrEqualToSuccessfully() string.CompareOrdinal(x.SortKey, sortKeyThreshold) <= 0); queriedItems.ShouldBe(expectedItems, ignoreOrder: true); } + + [Test] + public void ThrowWhenInvalidQueryParameters() + { + Should.Throw(async () => + { + await _context.Query() + .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithTableName("non_existent_table") + .ToAsyncEnumerable() + .ToListAsync(); + }); + } + + [Test] + public async Task QueryItemsWhenSuppressedThrowing() + { + var result = await _context.Query() + .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithLimit(2) + .SuppressThrowing() + .ToPageAsync(); + + result.IsSuccess.ShouldBeTrue(); + result.Value.Items.Count.ShouldBeLessThanOrEqualTo(2); + } + + [Test] + public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() + { + var result = await _context.Query() + .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithTableName("non_existent_table") + .SuppressThrowing() + .ToPageAsync(); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + } } \ No newline at end of file From be2658d09e879f60c5941f09c7d4cf4a32dbcf55 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 21:39:36 +0200 Subject: [PATCH 21/28] BatchGet integration tests --- .../DataPlane/BatchGet/BatchGetShould.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchGet/BatchGetShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchGet/BatchGetShould.cs index 41f259cc..feba5994 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchGet/BatchGetShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchGet/BatchGetShould.cs @@ -1,3 +1,5 @@ +using EfficientDynamoDb.Extensions; +using EfficientDynamoDb.Exceptions; using EfficientDynamoDb.Internal.Extensions; using EfficientDynamoDb.Operations.Shared; using NUnit.Framework; @@ -218,4 +220,48 @@ public async Task SupportLargeBatchGetRequest() result.ShouldBe(_testUsers, ignoreOrder: true); } + + [Test] + public void ThrowWhenInvalidBatchGetParameters() + { + Should.Throw(async () => + { + await _context.BatchGet() + .WithItems( + Batch.GetItem().WithPrimaryKey($"{KeyPrefix}-pk-1", ""), + Batch.GetItem().WithPrimaryKey($"{KeyPrefix}-pk-2", "") + ) + .ToListAsync(); + }); + } + + [Test] + public async Task BatchGetItemsWhenSuppressedThrowing() + { + var result = await _context.BatchGet() + .WithItems( + Batch.GetItem().WithPrimaryKey(_testUsers[0].PartitionKey, _testUsers[0].SortKey), + Batch.GetItem().WithPrimaryKey(_testUsers[1].PartitionKey, _testUsers[1].SortKey) + ) + .SuppressThrowing() + .ToListAsync(); + + result.IsSuccess.ShouldBeTrue(); + result.Value.ShouldBe(_testUsers[..2], ignoreOrder: true); + } + + [Test] + public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() + { + var result = await _context.BatchGet() + .WithItems( + Batch.GetItem().WithPrimaryKey($"{KeyPrefix}-pk-1", ""), + Batch.GetItem().WithPrimaryKey($"{KeyPrefix}-pk-2", "") + ) + .SuppressThrowing() + .ToListAsync(); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + } } \ No newline at end of file From 2a6c45bcfc30f7394610481be4025f27ec93bdf7 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 21:44:25 +0200 Subject: [PATCH 22/28] BatchWrite integration tests --- .../DataPlane/BatchWrite/BatchWriteShould.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs index 5cf03d8b..57e82531 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs @@ -1,3 +1,4 @@ +using EfficientDynamoDb.Exceptions; using EfficientDynamoDb.Operations.Shared; using NUnit.Framework; using Shouldly; @@ -460,4 +461,83 @@ await _context.BatchWrite() retrievedReplacementUser!.Name.ShouldBe("Replacement Complex User"); retrievedReplacementUser.Age.ShouldBe(35); } + + [Test] + public void ThrowWhenInvalidBatchWriteParameters() + { + var invalidUsers = new[] + { + new TestUser { PartitionKey = $"{KeyPrefix}-pk-1", SortKey = "", Name = "test", Age = 25, Email = "test@example.com" }, + new TestUser { PartitionKey = $"{KeyPrefix}-pk-2", SortKey = "", Name = "test2", Age = 30, Email = "test2@example.com" } + }; + + _testUsersToCleanup.AddRange(invalidUsers); + + Should.Throw(async () => + { + await _context.BatchWrite() + .WithItems(invalidUsers.Select(Batch.PutItem)) + .ExecuteAsync(); + }); + } + + [Test] + public async Task BatchWriteItemsWhenSuppressedThrowing() + { + var testUsers = new[] + { + new TestUser + { + PartitionKey = $"{KeyPrefix}-suppressed-pk-1", + SortKey = $"{KeyPrefix}-suppressed-sk-1", + Name = "Suppressed User 1", + Age = 25, + Email = "suppressed1@example.com" + }, + new TestUser + { + PartitionKey = $"{KeyPrefix}-suppressed-pk-2", + SortKey = $"{KeyPrefix}-suppressed-sk-2", + Name = "Suppressed User 2", + Age = 30, + Email = "suppressed2@example.com" + } + }; + + _testUsersToCleanup.AddRange(testUsers); + + var result = await _context.BatchWrite() + .WithItems(testUsers.Select(Batch.PutItem)) + .SuppressThrowing() + .ExecuteAsync(); + + result.IsSuccess.ShouldBeTrue(); + + // Verify items were created using batch read + var retrievedItems = await _context.BatchGet() + .WithItems(testUsers.Select(user => Batch.GetItem().WithPrimaryKey(user.PartitionKey, user.SortKey))) + .ToListAsync(); + + retrievedItems.ShouldBe(testUsers, ignoreOrder: true); + } + + [Test] + public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() + { + var invalidUsers = new[] + { + new TestUser { PartitionKey = $"{KeyPrefix}-suppressed-error-pk-1", SortKey = "", Name = "test", Age = 25, Email = "test@example.com" }, + new TestUser { PartitionKey = $"{KeyPrefix}-suppressed-error-pk-2", SortKey = "", Name = "test2", Age = 30, Email = "test2@example.com" } + }; + + _testUsersToCleanup.AddRange(invalidUsers); + + var result = await _context.BatchWrite() + .WithItems(invalidUsers.Select(Batch.PutItem)) + .SuppressThrowing() + .ExecuteAsync(); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + } } \ No newline at end of file From 092666d2ec56ac9d1a9d60e6a9084d80bcfc89c6 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sat, 7 Jun 2025 21:57:14 +0200 Subject: [PATCH 23/28] Fix integration tests --- .../DataPlane/BatchWrite/BatchWriteShould.cs | 4 ---- .../DataPlane/Query/QueryShould.cs | 14 +++++++------- .../DataPlane/Scan/ScanShould.cs | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs index 57e82531..82c2870b 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/BatchWrite/BatchWriteShould.cs @@ -471,8 +471,6 @@ public void ThrowWhenInvalidBatchWriteParameters() new TestUser { PartitionKey = $"{KeyPrefix}-pk-2", SortKey = "", Name = "test2", Age = 30, Email = "test2@example.com" } }; - _testUsersToCleanup.AddRange(invalidUsers); - Should.Throw(async () => { await _context.BatchWrite() @@ -530,8 +528,6 @@ public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() new TestUser { PartitionKey = $"{KeyPrefix}-suppressed-error-pk-2", SortKey = "", Name = "test2", Age = 30, Email = "test2@example.com" } }; - _testUsersToCleanup.AddRange(invalidUsers); - var result = await _context.BatchWrite() .WithItems(invalidUsers.Select(Batch.PutItem)) .SuppressThrowing() diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs index 910707ef..2e0ae8b5 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Query/QueryShould.cs @@ -442,10 +442,10 @@ public async Task QueryItemsWithSortKeyLessThanOrEqualToSuccessfully() [Test] public void ThrowWhenInvalidQueryParameters() { - Should.Throw(async () => + Should.Throw(async () => { await _context.Query() - .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithKeyExpression(x => x.On(y => y.PartitionKey).EqualTo($"{KeyPrefix}-pk-1")) .WithTableName("non_existent_table") .ToAsyncEnumerable() .ToListAsync(); @@ -456,20 +456,20 @@ await _context.Query() public async Task QueryItemsWhenSuppressedThrowing() { var result = await _context.Query() - .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) - .WithLimit(2) + .WithKeyExpression(x => x.On(y => y.PartitionKey).EqualTo($"{KeyPrefix}-pk-1")) .SuppressThrowing() - .ToPageAsync(); + .ToListAsync(); + result.Exception.ShouldBeNull(); result.IsSuccess.ShouldBeTrue(); - result.Value.Items.Count.ShouldBeLessThanOrEqualTo(2); + result.Value.ShouldBe(_testUsers.Where(x => x.PartitionKey == $"{KeyPrefix}-pk-1"), ignoreOrder: true); } [Test] public async Task ReturnErrorWhenInvalidRequestAndSuppressedThrowing() { var result = await _context.Query() - .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) + .WithKeyExpression(x => x.On(y => y.PartitionKey).EqualTo($"{KeyPrefix}-pk-1")) .WithTableName("non_existent_table") .SuppressThrowing() .ToPageAsync(); diff --git a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs index d7955c2c..a7c8ab70 100644 --- a/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs +++ b/src/EfficientDynamoDb.IntegrationTests/DataPlane/Scan/ScanShould.cs @@ -330,7 +330,7 @@ public async Task ScanWithStringContainsFilterSuccessfully() [Test] public void ThrowWhenInvalidScanParameters() { - Should.Throw(async () => + Should.Throw(async () => { await _context.Scan() .WithFilterExpression(x => x.On(y => y.PartitionKey).BeginsWith($"{KeyPrefix}-pk-")) From 4c128cede7c5dd73b0ec2bcd3536629dade1c215 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sun, 22 Jun 2025 15:09:26 +0200 Subject: [PATCH 24/28] Add api reference for SuppressThrowing --- .../builders/delete-item-builder.md | 26 +++++++++++++++++ .../builders/get-item-builder.md | 29 ++++++++++++++++++- .../builders/put-item-builder.md | 27 +++++++++++++++++ .../api_reference/builders/query-builder.md | 27 +++++++++++++++++ .../api_reference/builders/scan-builder.md | 29 +++++++++++++++++++ .../builders/update-item-builder.md | 27 +++++++++++++++++ 6 files changed, 164 insertions(+), 1 deletion(-) diff --git a/website/docs/api_reference/builders/delete-item-builder.md b/website/docs/api_reference/builders/delete-item-builder.md index 31b5e25d..6cf38e8a 100644 --- a/website/docs/api_reference/builders/delete-item-builder.md +++ b/website/docs/api_reference/builders/delete-item-builder.md @@ -200,6 +200,31 @@ var documentBuilder = builder.AsDocument(); After execution, this `DeleteItem` request will return the `Document` instead of the original entity of the builder. +### SuppressThrowing {#suppressthrowing} + +Prevents the `DeleteItem` operation from throwing an exception in case of any failure. Instead, the execution methods will return an `OpResult` or `OpResult` that encapsulates either a successful result or an error. + +This method returns a different type of the builder to indicate that exception suppression is active. + +```csharp +ISuppressedDeleteItemEntityRequestBuilder SuppressThrowing(); +``` + +#### Example {#suppressthrowing-example} + +```csharp +var result = await builder.SuppressThrowing().ExecuteAsync(); +if (result.IsSuccess) +{ + // Handle successful execution. +} +else +{ + var exception = result.Exception; + // Handle error. +} +``` + ## DeleteItem Execution There are 2 versions of every query execution method: regular and document. @@ -207,6 +232,7 @@ All versions have same parameters, the only difference is entity type returned v - In most cases, the original entity `TEntity` is returned. - If `AsDocuments()` was used, the execution method will contain the entity type of `Document`. +- If `SuppressThrowing()` was used, the execution method will return an `OpResult` where `T` is one of the types above. For simplicity, this document covers only regular version of execution methods. diff --git a/website/docs/api_reference/builders/get-item-builder.md b/website/docs/api_reference/builders/get-item-builder.md index 1ffb6675..0e2324e3 100644 --- a/website/docs/api_reference/builders/get-item-builder.md +++ b/website/docs/api_reference/builders/get-item-builder.md @@ -214,6 +214,32 @@ var documentBuilder = builder.AsDocument(); After execution, this `GetItem` request will return the `Document` instead of the original entity of the builder. +### SuppressThrowing {#suppressthrowing} + +Prevents the `GetItem` operation from throwing an exception in case of any failure. Instead, the execution methods will return an `OpResult` that encapsulates either a successful result or an error. + +Similarly to [`AsProjection()`](#asprojection-type), this method returns a different type of the builder to indicate that exception suppression is active. + +```csharp +ISuppressedGetItemEntityRequestBuilder SuppressThrowing(); +``` + +#### Example {#suppressthrowing-example} + +```csharp +var result = builder.SuppressThrowing().ToItemAsync(); +if (result.IsSuccess) +{ + var item = result.Value; + // process item +} +else +{ + var exception = result.Exception; + // handle error +} +``` + ## GetItem Execution There are 3 versions of every GetItem execution method: regular, projected, and document. @@ -222,8 +248,9 @@ All versions have same parameters, the only difference is entity type returned v - In most cases, the original entity `TEntity` is returned. - If `AsProjection()` was used during the configuration, the execution method will contain the entity type of `TProjection`. - If `AsDocument()` was used, the execution method will contain the entity type of `Document`. +- If `SuppressThrowing()` was used, the execution method will return an `OpResult` where `T` is one of the types above. -In all cases, the result will be `null` if the item does not exist. +In all cases where `SuppressThrowing()` is not used, the result will be `null` if the item does not exist. When `SuppressThrowing()` is used, `OpResult.Value` will be `null`. For simplicity, this document covers only regular version of execution methods. diff --git a/website/docs/api_reference/builders/put-item-builder.md b/website/docs/api_reference/builders/put-item-builder.md index 64cd028d..20d0318d 100644 --- a/website/docs/api_reference/builders/put-item-builder.md +++ b/website/docs/api_reference/builders/put-item-builder.md @@ -172,6 +172,32 @@ var documentBuilder = builder.AsDocument(); After execution, this `PutItem` request will return the `Document` instead of the original entity of the builder. +### SuppressThrowing {#suppressthrowing} + +Prevents the `PutItem` operation from throwing an exception in case of any failure. Instead, the execution methods will return an `OpResult` or `OpResult` that encapsulates either a successful result or an error. + +This method returns a different type of the builder to indicate that exception suppression is active. + +```csharp +ISuppressedPutItemEntityRequestBuilder SuppressThrowing(); +``` + +#### Example {#suppressthrowing-example} + +```csharp +var result = await builder.SuppressThrowing().ExecuteAsync(); +if (result.IsSuccess) +{ + // Handle successful execution. + +} +else +{ + var exception = result.Exception; + // Handle error. +} +``` + ## PutItem Execution There are 2 versions of every query execution method: regular and document. @@ -179,6 +205,7 @@ All versions have same parameters, the only difference is entity type returned v - In most cases, the original entity `TEntity` is returned. - If `AsDocuments()` was used, the execution method will contain the entity type of `Document`. +- If `SuppressThrowing()` was used, the execution method will return an `OpResult` where `T` is one of the types above. For simplicity, this document covers only regular version of execution methods. diff --git a/website/docs/api_reference/builders/query-builder.md b/website/docs/api_reference/builders/query-builder.md index 56661f6e..ec60c526 100644 --- a/website/docs/api_reference/builders/query-builder.md +++ b/website/docs/api_reference/builders/query-builder.md @@ -355,6 +355,32 @@ IQueryDocumentRequestBuilder AsDocuments(); var documentBuilder = builder.AsDocuments(); ``` +### SuppressThrowing {#suppressthrowing} + +Prevents the `Query` operation from throwing an exception in case of any failure. Instead, the execution methods will return an `OpResult` that encapsulates either a successful result or an error. + +This method returns a different type of the builder to indicate that exception suppression is active. + +```csharp +ISuppressedQueryEntityRequestBuilder SuppressThrowing(); +``` + +#### Example {#suppressthrowing-example} + +```csharp +var result = await builder.SuppressThrowing().ToListAsync(); +if (result.IsSuccess) +{ + var items = result.Value; + // process items +} +else +{ + var exception = result.Exception; + // handle error +} +``` + ## Query Execution There are 3 versions of every query execution method: regular, projected, and document. @@ -363,6 +389,7 @@ All versions have same parameters, the only difference is entity type returned v - In most cases, the original entity `TEntity` is returned. - If `AsProjection()` was used during the configuration, the execution method will contain the entity type of `TProjection`. - If `AsDocuments()` was used, the execution method will contain the entity type of `Document`. +- If `SuppressThrowing()` was used, the execution method will return an `OpResult` where `T` is one of the types above. For simplicity, this document covers only regular version of execution methods. diff --git a/website/docs/api_reference/builders/scan-builder.md b/website/docs/api_reference/builders/scan-builder.md index f5e10a78..710bcc07 100644 --- a/website/docs/api_reference/builders/scan-builder.md +++ b/website/docs/api_reference/builders/scan-builder.md @@ -308,6 +308,34 @@ IScanDocumentRequestBuilder AsDocuments(); var documentBuilder = builder.AsDocuments(); ``` +### SuppressThrowing {#suppressthrowing} + +Prevents the `Scan` operation from throwing an exception in case of any failure. Instead, the execution methods will return an `OpResult` that encapsulates either a successful result or an error. + +This method returns a different type of the builder to indicate that exception suppression is active. + +```csharp +ISuppressedScanEntityRequestBuilder SuppressThrowing(); +``` + +#### Example {#suppressthrowing-example} + +```csharp +var result = await builder.SuppressThrowing().ToPageAsync(); +if (result.IsSuccess) +{ + var page = result.Value; + var items = page.Items; + var paginationToken = page.PaginationToken; + // process page +} +else +{ + var exception = result.Exception; + // handle error +} +``` + ## Scan Execution There are 3 versions of every query execution method: regular, projected, and document. @@ -316,6 +344,7 @@ All versions have same parameters, the only difference is entity type returned v - In most cases, the original entity `TEntity` is returned. - If `AsProjection()` was used during the configuration, the execution method will contain the entity type of `TProjection`. - If `AsDocuments()` was used, the execution method will contain the entity type of `Document`. +- If `SuppressThrowing()` was used, the execution method will return an `OpResult` where `T` is one of the types above. For simplicity, this document covers only regular version of execution methods. diff --git a/website/docs/api_reference/builders/update-item-builder.md b/website/docs/api_reference/builders/update-item-builder.md index 812b20c0..2ff4568b 100644 --- a/website/docs/api_reference/builders/update-item-builder.md +++ b/website/docs/api_reference/builders/update-item-builder.md @@ -199,6 +199,32 @@ var documentBuilder = builder.AsDocument(); After execution, this `UpdateItem` request will return the `Document` instead of the original entity of the builder. +### SuppressThrowing {#suppressthrowing} + +Prevents the `UpdateItem` operation from throwing an exception in case of any failure. Instead, the execution methods will return an `OpResult` that encapsulates either a successful result or an error. + +This method returns a different type of the builder to indicate that exception suppression is active. + +```csharp +ISuppressedUpdateItemEntityRequestBuilder SuppressThrowing(); +``` + +#### Example {#suppressthrowing-example} + +```csharp +var result = await builder.SuppressThrowing().ToItemAsync(); +if (result.IsSuccess) +{ + var item = result.Value; + // process item +} +else +{ + var exception = result.Exception; + // handle error +} +``` + ### On {#on} Specifies the attribute to be updated in the DynamoDB item. @@ -228,6 +254,7 @@ All versions have same parameters, the only difference is entity type returned v - In most cases, the original entity `TEntity` is returned. - If `AsDocuments()` was used, the execution method will contain the entity type of `Document`. +- If `SuppressThrowing()` was used, the execution method will return an `OpResult` where `T` is one of the types above. For simplicity, this document covers only regular version of execution methods. From f78233fe75d3502daacb96fb7a2e008785f03b6f Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Sun, 22 Jun 2025 18:23:30 +0200 Subject: [PATCH 25/28] Add docs for SuppressThrowing functionality --- .../error_handling/op-error-type.md | 30 +++++++ .../api_reference/error_handling/op-result.md | 90 +++++++++++++++++++ .../dev_guide/high_level/error-handling.md | 81 +++++++++++++++++ website/sidebars.js | 16 +++- 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 website/docs/api_reference/error_handling/op-error-type.md create mode 100644 website/docs/api_reference/error_handling/op-result.md create mode 100644 website/docs/dev_guide/high_level/error-handling.md diff --git a/website/docs/api_reference/error_handling/op-error-type.md b/website/docs/api_reference/error_handling/op-error-type.md new file mode 100644 index 00000000..ef5498e3 --- /dev/null +++ b/website/docs/api_reference/error_handling/op-error-type.md @@ -0,0 +1,30 @@ +--- +id: op-error-type +title: OpErrorType +slug: ../../api-reference/error-handling/op-error-type +--- + +`OpErrorType` is an enum that represents the type of error that occurred during an operation. +Used in [`OpResult`](op-result.md#op-result) and [`OpResult`](op-result.md#op-result-t) to indicate the type of error that occurred during an operation. + +The possible values are: +- `None` +- `Unknown` +- `ServiceUnavailable` +- `InternalServerError` +- `TransactionCanceled` +- `ConditionalCheckFailed` +- `ProvisionedThroughputExceeded` +- `AccessDenied` +- `IncompleteSignature` +- `ItemCollectionSizeLimitExceeded` +- `LimitExceeded` +- `MissingAuthenticationToken` +- `RequestLimitExceeded` +- `ResourceInUse` +- `ResourceNotFound` +- `Throttling` +- `UnrecognizedClient` +- `Validation` +- `IdempotentParameterMismatch` +- `TransactionInProgress` \ No newline at end of file diff --git a/website/docs/api_reference/error_handling/op-result.md b/website/docs/api_reference/error_handling/op-result.md new file mode 100644 index 00000000..bb337f1e --- /dev/null +++ b/website/docs/api_reference/error_handling/op-result.md @@ -0,0 +1,90 @@ +--- +id: op-result +title: OpResult +slug: ../../api-reference/error-handling/op-result +--- + +## Summary + +`OpResult` and `OpResult` are used to encapsulate the result of an operation when `SuppressThrowing` builder method is used for any Data Plane operation. + +## `OpResult` {#op-result} + +A read-only struct that contains the status of an operation that doesn't return a value. + +```csharp +[StructLayout(LayoutKind.Auto)] +public readonly struct OpResult +``` + +### Properties {#op-result-properties} + +| Property | Data Type | Description | +|-------------|---------------|-------------------------------------------------------------| +| `ErrorType` | [`OpErrorType`](op-error-type.md) | Contains the type of error that occurred during the operation. | +| `Exception` | `DdbException?` | Contains the exception that occurred during the operation. `null` if `IsSuccess` is `true`. | +| `IsSuccess` | `bool` | Indicates whether the operation was successful. | + +### Methods {#op-result-methods} + +| Method | Description | +|--------|-------------| +| `EnsureSuccess()` | Throws an exception if the operation failed. | + +There are also extension methods to convert the `OpResult` to a specific exception type. See [Exception conversion methods](#exception-conversion-methods) below. + +## `OpResult` {#op-result-t} + +A read-only struct that contains the result of an operation that returns a value. + +```csharp +[StructLayout(LayoutKind.Auto)] +public readonly struct OpResult +``` + +### Properties {#op-result-t-properties} + +| Property | Data Type | Description | +|-------------|---------------|-------------------------------------------------------------| +| `ErrorType` | [`OpErrorType`](op-error-type.md) | Contains the type of error that occurred during the operation. | +| `Exception` | `DdbException?` | Contains the exception that occurred during the operation. `null` if `IsSuccess` is `true`. | +| `IsSuccess` | `bool` | Indicates whether the operation was successful. | +| `Value` | `T?` | Contains the result of the operation if `IsSuccess` is `true`, otherwise `default(T)`. | + +### Methods {#op-result-t-methods} + +| Method | Description | +|--------|-------------| +| `EnsureSuccess()` | Throws an exception if the operation failed. | + +Similar to [`OpResult` methods](#op-result-methods), there are also extension methods to convert the `OpResult` to a specific exception type. See [Exception conversion methods](#exception-conversion-methods) below. + +## Exception conversion methods {#exception-conversion-methods} + +These extension methods are used to convert the `OpResult` and `OpResult` to a specific exception type. + +:::note +These methods will throw `InvalidOperationException` if an actual exception type doesn't match the called method. +Use `ErrorType` to check the type of the error before calling these methods. +::: + +| Method | Description | +|--------|-------------| +| `AsServiceUnavailableException()` | Returns a `ServiceUnavailableException` if the operation failed. | +| `AsInternalServerErrorException()` | Returns a `InternalServerErrorException` if the operation failed. | +| `AsTransactionCanceledException()` | Returns a `TransactionCanceledException` if the operation failed. | +| `AsConditionalCheckFailedException()` | Returns a `ConditionalCheckFailedException` if the operation failed. | +| `AsProvisionedThroughputExceededException()` | Returns a `ProvisionedThroughputExceededException` if the operation failed. | +| `AsAccessDeniedException()` | Returns a `AccessDeniedException` if the operation failed. | +| `AsIncompleteSignatureException()` | Returns a `IncompleteSignatureException` if the operation failed. | +| `AsItemCollectionSizeLimitExceededException()` | Returns a `ItemCollectionSizeLimitExceededException` if the operation failed. | +| `AsLimitExceededException()` | Returns a `LimitExceededException` if the operation failed. | +| `AsMissingAuthenticationTokenException()` | Returns a `MissingAuthenticationTokenException` if the operation failed. | +| `AsRequestLimitExceededException()` | Returns a `RequestLimitExceededException` if the operation failed. | +| `AsResourceInUseException()` | Returns a `ResourceInUseException` if the operation failed. | +| `AsResourceNotFoundException()` | Returns a `ResourceNotFoundException` if the operation failed. | +| `AsThrottlingException()` | Returns a `ThrottlingException` if the operation failed. | +| `AsUnrecognizedClientException()` | Returns a `UnrecognizedClientException` if the operation failed. | +| `AsValidationException()` | Returns a `ValidationException` if the operation failed. | +| `AsIdempotentParameterMismatchException()` | Returns a `IdempotentParameterMismatchException` if the operation failed. | +| `AsTransactionInProgressException()` | Returns a `TransactionInProgressException` if the operation failed. | \ No newline at end of file diff --git a/website/docs/dev_guide/high_level/error-handling.md b/website/docs/dev_guide/high_level/error-handling.md new file mode 100644 index 00000000..d1ce8cda --- /dev/null +++ b/website/docs/dev_guide/high_level/error-handling.md @@ -0,0 +1,81 @@ +--- +id: error-handling +title: Error handling +slug: ../../dev-guide/high-level/error-handling +--- + +# Error handling {#error-handling} + +EfficientDynamoDb supports two ways of handling DynamoDB errors: +- Throwing exceptions (default behavior) +- Suppressing exceptions and returning [`OpResult`](../../api_reference/error_handling/op-result.md#op-result) or [`OpResult`](../../api_reference/error_handling/op-result.md#op-result-t). + +:::note +Both approaches utilize [retry strategies](../configuration/retry-strategies.md) before throwing an exception or returning an error result. +::: + +## Throwing exceptions {#throwing-exceptions} + +Throwing exceptions is the default behavior. +After encountering an error and after all retries are exhausted, an `DdbException` is thrown. + +## Returning errors as results {#returning-errors-as-results} + +This behavior can be useful when some error is expected to happen, e.g. when table is throttled. +To enable this behavior, use `SuppressThrowing()` method with a builder API for any data plane operation: +- [GetItem](../../api_reference/builders/get-item-builder.md#suppressthrowing) +- [PutItem](../../api_reference/builders/put-item-builder.md#suppressthrowing) +- [UpdateItem](../../api_reference/builders/update-item-builder.md#suppressthrowing) +- [DeleteItem](../../api_reference/builders/delete-item-builder.md#suppressthrowing) +- BatchGetItem +- BatchWriteItem +- [Query](../../api_reference/builders/query-builder.md#suppressthrowing) +- [Scan](../../api_reference/builders/scan-builder.md#suppressthrowing) +- TransactGetItems +- TransactWriteItems + +### `OpResult` vs `OpResult` + +After applying `SuppressThrowing()`, executing operations will return [`OpResult`](../../api_reference/error_handling/op-result.md#op-result) or [`OpResult`](../../api_reference/error_handling/op-result.md#op-result-t) instead of throwing an exception. +The only difference between these two types is that `OpResult` contains the result of the operation, while `OpResult` doesn't: +- `ExecuteAsync()` always returns `OpResult`. +- `OpResult` is returned when `ToItemAsync()`, `ToResponseAsync()`, `ToListAsync()` or similar methods are called. + +### Handling error results {#handling-error-results} + +There are two main ways to handle error results. + +#### Checking `ErrorType` and using `As` methods + +```csharp +var result = await context.GetItem() + .WithPrimaryKey("pk", "sk") + .SuppressThrowing() + .ToItemAsync(); + +if (result.IsSuccess) + return result.Value; + +if (result.ErrorType == OpErrorType.ProvisionedThroughputExceeded) +{ + var provisionedException = result.AsProvisionedThroughputExceededException(); + // Handle provisioned throughput exceeded error +} +``` + +#### Using pattern matching + +```csharp +var result = await context.GetItem() + .WithPrimaryKey("pk", "sk") + .SuppressThrowing() + .ToItemAsync(); + +if (result.IsSuccess) + return result.Value; + +if (result.Exception is ProvisionedThroughputExceededException provisionedException) +{ + // Handle provisioned throughput exceeded error +} +``` \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index e277705d..3418d991 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -23,6 +23,7 @@ module.exports = { 'dev_guide/high_level/converters', 'dev_guide/high_level/conditions', 'dev_guide/high_level/update-expression', + 'dev_guide/high_level/error-handling', ], }, 'dev_guide/low-level', @@ -58,7 +59,20 @@ module.exports = { 'api_reference/options/select-mode', 'api_reference/options/return-values', ], - } + }, + { + type: 'category', + label: 'Error Handling', + link: { + type: 'generated-index', + title: 'Error Handling', + slug: 'api-reference/error-handling', + }, + items: [ + 'api_reference/error_handling/op-result', + 'api_reference/error_handling/op-error-type', + ], + }, ], }, }; From 379b9ba7891c29d999d09632cd4c0db05ef56477 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Mon, 23 Jun 2025 01:03:30 +0200 Subject: [PATCH 26/28] Refactor OpResult and add tests --- .../ErrorHandling/OpResultShould.cs | 104 ++++++++ .../ErrorHandling/OpResultWithValueShould.cs | 128 ++++++++++ .../EfficientDynamoDb.csproj.DotSettings | 1 + src/EfficientDynamoDb/Operations/OpResult.cs | 222 ------------------ .../Operations/ResultHandling/OpErrorType.cs | 25 ++ .../Operations/ResultHandling/OpResult.cs | 72 ++++++ .../ResultHandling/OpResultExtensions.cs | 83 +++++++ 7 files changed, 413 insertions(+), 222 deletions(-) create mode 100644 src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultShould.cs create mode 100644 src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultWithValueShould.cs delete mode 100644 src/EfficientDynamoDb/Operations/OpResult.cs create mode 100644 src/EfficientDynamoDb/Operations/ResultHandling/OpErrorType.cs create mode 100644 src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs create mode 100644 src/EfficientDynamoDb/Operations/ResultHandling/OpResultExtensions.cs diff --git a/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultShould.cs b/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultShould.cs new file mode 100644 index 00000000..9ed1e9ce --- /dev/null +++ b/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultShould.cs @@ -0,0 +1,104 @@ +using System; +using EfficientDynamoDb.Exceptions; +using EfficientDynamoDb.Operations; +using NUnit.Framework; +using Shouldly; + +namespace EfficientDynamoDb.Tests.Operations.ErrorHandling; + +[TestFixture] +public class OpResultShould +{ + [Test] + public void BeInSuccessStateWhenExceptionIsNotSet() + { + var result = new OpResult(); + + result.IsSuccess.ShouldBeTrue(); + result.Exception.ShouldBeNull(); + result.ErrorType.ShouldBe(OpErrorType.None); + } + + [Test] + public void BeInFailureStateWhenExceptionIsSet() + { + var result = new OpResult(new ProvisionedThroughputExceededException("error")); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldNotBeNull(); + result.ErrorType.ShouldBe(OpErrorType.ProvisionedThroughputExceeded); + } + + [Test] + public void NotThrowExceptionWhenEnsureSuccessIsCalledOnSuccessResult() + { + var result = new OpResult(); + Should.NotThrow(result.EnsureSuccess); + } + + [Test] + public void ThrowExceptionWhenEnsureSuccessIsCalledOnFailureResult() + { + var result = new OpResult(new ProvisionedThroughputExceededException("error")); + + Should.Throw(result.EnsureSuccess); + } + + [Test] + public void ThrowExceptionWhenIncorrectAsMethodIsCalled() + { + var result = new OpResult(new ProvisionedThroughputExceededException("error")); + + Should.Throw(() => result.AsInternalServerErrorException()); + } + + [TestCaseSource(nameof(AsExceptionCases))] + public void ReturnCorrectExceptionWhenAsMethodIsCalled(DdbException expectedException, Func asFunc) + { + var result = new OpResult(expectedException); + + var actualException = asFunc(result); + + actualException.ShouldBe(expectedException); + } + + public static TestCaseData>[] AsExceptionCases() => + [ + new(new ServiceUnavailableException("error"), r => r.AsServiceUnavailableException()) + { TestName = "ServiceUnavailableException" }, + new(new InternalServerErrorException("error"), r => r.AsInternalServerErrorException()) + { TestName = "InternalServerErrorException" }, + new(new TransactionCanceledException([], "error"), r => r.AsTransactionCanceledException()) + { TestName = "TransactionCanceledException" }, + new(new ConditionalCheckFailedException(null, "error"), r => r.AsConditionalCheckFailedException()) + { TestName = "ConditionalCheckFailedException" }, + new(new ProvisionedThroughputExceededException("error"), r => r.AsProvisionedThroughputExceededException()) + { TestName = "ProvisionedThroughputExceededException" }, + new(new AccessDeniedException("error"), r => r.AsAccessDeniedException()) + { TestName = "AccessDeniedException" }, + new(new IncompleteSignatureException("error"), r => r.AsIncompleteSignatureException()) + { TestName = "IncompleteSignatureException" }, + new(new ItemCollectionSizeLimitExceededException("error"), r => r.AsItemCollectionSizeLimitExceededException()) + { TestName = "ItemCollectionSizeLimitExceededException" }, + new(new LimitExceededException("error"), r => r.AsLimitExceededException()) + { TestName = "LimitExceededException" }, + new(new MissingAuthenticationTokenException("error"), r => r.AsMissingAuthenticationTokenException()) + { TestName = "MissingAuthenticationTokenException" }, + new(new RequestLimitExceededException("error"), r => r.AsRequestLimitExceededException()) + { TestName = "RequestLimitExceededException" }, + new(new ResourceInUseException("error"), r => r.AsResourceInUseException()) + { TestName = "ResourceInUseException" }, + new(new ResourceNotFoundException("error"), r => r.AsResourceNotFoundException()) + { TestName = "ResourceNotFoundException" }, + new(new ThrottlingException("error"), r => r.AsThrottlingException()) + { TestName = "ThrottlingException" }, + new(new UnrecognizedClientException("error"), r => r.AsUnrecognizedClientException()) + { TestName = "UnrecognizedClientException" }, + new(new ValidationException("error"), r => r.AsValidationException()) + { TestName = "ValidationException" }, + new(new IdempotentParameterMismatchException("error"), r => r.AsIdempotentParameterMismatchException()) + { TestName = "IdempotentParameterMismatchException" }, + new(new TransactionInProgressException("error"), r => r.AsTransactionInProgressException()) + { TestName = "TransactionInProgressException" } + ]; +} \ No newline at end of file diff --git a/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultWithValueShould.cs b/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultWithValueShould.cs new file mode 100644 index 00000000..efca39f0 --- /dev/null +++ b/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultWithValueShould.cs @@ -0,0 +1,128 @@ +using System; +using EfficientDynamoDb.Exceptions; +using EfficientDynamoDb.Operations; +using NUnit.Framework; +using Shouldly; + +namespace EfficientDynamoDb.Tests.Operations.ErrorHandling; + +[TestFixture] +public class OpResultWithValueShould +{ + [Test] + public void BeInSuccessStateWhenValueIsSet() + { + var result = new OpResult("test"); + + result.IsSuccess.ShouldBeTrue(); + result.Exception.ShouldBeNull(); + result.ErrorType.ShouldBe(OpErrorType.None); + result.Value.ShouldBe("test"); + } + + [Test] + public void BeInFailureStateWhenExceptionIsSet() + { + var exception = new ProvisionedThroughputExceededException("error"); + var result = new OpResult(exception); + + result.IsSuccess.ShouldBeFalse(); + result.Exception.ShouldBe(exception); + result.ErrorType.ShouldBe(OpErrorType.ProvisionedThroughputExceeded); + result.Value.ShouldBeNull(); + } + + [Test] + public void ReturnValueWhenEnsureSuccessIsCalledOnSuccessResult() + { + var result = new OpResult("test"); + + var value = Should.NotThrow(result.EnsureSuccess); + + value.ShouldBe("test"); + } + + [Test] + public void ThrowExceptionWhenEnsureSuccessIsCalledOnFailureResult() + { + var exception = new ProvisionedThroughputExceededException("error"); + var result = new OpResult(exception); + + Should.Throw(() => result.EnsureSuccess()).ShouldBe(exception); + } + + [Test] + public void ReturnOpResultWhenDiscardValueIsCalledForSuccessfulState() + { + var result = new OpResult("test"); + + var discardedResult = result.DiscardValue(); + + discardedResult.IsSuccess.ShouldBeTrue(); + discardedResult.Exception.ShouldBeNull(); + discardedResult.ErrorType.ShouldBe(OpErrorType.None); + } + + [Test] + public void ReturnOpResultWhenDiscardValueIsCalledForFailureState() + { + var exception = new ProvisionedThroughputExceededException("error"); + var result = new OpResult(exception); + + var discardedResult = result.DiscardValue(); + + discardedResult.IsSuccess.ShouldBeFalse(); + discardedResult.Exception.ShouldBe(exception); + discardedResult.ErrorType.ShouldBe(OpErrorType.ProvisionedThroughputExceeded); + } + + [TestCaseSource(nameof(AsExceptionCases))] + public void ReturnCorrectExceptionWhenAsMethodIsCalled(DdbException expectedException, Func, DdbException> asFunc) + { + var result = new OpResult(expectedException); + + var actualException = asFunc(result); + + actualException.ShouldBe(expectedException); + } + + public static TestCaseData, DdbException>>[] AsExceptionCases() => + [ + new(new ServiceUnavailableException("error"), r => r.AsServiceUnavailableException()) + { TestName = "ServiceUnavailableException" }, + new(new InternalServerErrorException("error"), r => r.AsInternalServerErrorException()) + { TestName = "InternalServerErrorException" }, + new(new TransactionCanceledException([], "error"), r => r.AsTransactionCanceledException()) + { TestName = "TransactionCanceledException" }, + new(new ConditionalCheckFailedException(null, "error"), r => r.AsConditionalCheckFailedException()) + { TestName = "ConditionalCheckFailedException" }, + new(new ProvisionedThroughputExceededException("error"), r => r.AsProvisionedThroughputExceededException()) + { TestName = "ProvisionedThroughputExceededException" }, + new(new AccessDeniedException("error"), r => r.AsAccessDeniedException()) + { TestName = "AccessDeniedException" }, + new(new IncompleteSignatureException("error"), r => r.AsIncompleteSignatureException()) + { TestName = "IncompleteSignatureException" }, + new(new ItemCollectionSizeLimitExceededException("error"), r => r.AsItemCollectionSizeLimitExceededException()) + { TestName = "ItemCollectionSizeLimitExceededException" }, + new(new LimitExceededException("error"), r => r.AsLimitExceededException()) + { TestName = "LimitExceededException" }, + new(new MissingAuthenticationTokenException("error"), r => r.AsMissingAuthenticationTokenException()) + { TestName = "MissingAuthenticationTokenException" }, + new(new RequestLimitExceededException("error"), r => r.AsRequestLimitExceededException()) + { TestName = "RequestLimitExceededException" }, + new(new ResourceInUseException("error"), r => r.AsResourceInUseException()) + { TestName = "ResourceInUseException" }, + new(new ResourceNotFoundException("error"), r => r.AsResourceNotFoundException()) + { TestName = "ResourceNotFoundException" }, + new(new ThrottlingException("error"), r => r.AsThrottlingException()) + { TestName = "ThrottlingException" }, + new(new UnrecognizedClientException("error"), r => r.AsUnrecognizedClientException()) + { TestName = "UnrecognizedClientException" }, + new(new ValidationException("error"), r => r.AsValidationException()) + { TestName = "ValidationException" }, + new(new IdempotentParameterMismatchException("error"), r => r.AsIdempotentParameterMismatchException()) + { TestName = "IdempotentParameterMismatchException" }, + new(new TransactionInProgressException("error"), r => r.AsTransactionInProgressException()) + { TestName = "TransactionInProgressException" } + ]; +} \ No newline at end of file diff --git a/src/EfficientDynamoDb/EfficientDynamoDb.csproj.DotSettings b/src/EfficientDynamoDb/EfficientDynamoDb.csproj.DotSettings index 60f0c18c..41485335 100644 --- a/src/EfficientDynamoDb/EfficientDynamoDb.csproj.DotSettings +++ b/src/EfficientDynamoDb/EfficientDynamoDb.csproj.DotSettings @@ -6,4 +6,5 @@ True True True + True True \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/OpResult.cs b/src/EfficientDynamoDb/Operations/OpResult.cs deleted file mode 100644 index 03995994..00000000 --- a/src/EfficientDynamoDb/Operations/OpResult.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using EfficientDynamoDb.Exceptions; - -namespace EfficientDynamoDb.Operations -{ - // TODO: Apply proper nullability attributes after migration to .net8 - // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis - [StructLayout(LayoutKind.Auto)] - public readonly struct OpResult - { - public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; - - public DdbException? Exception { get; } - - public OpResult(DdbException? exception) - { - Exception = exception; - } - - public void EnsureSuccess() - { - if (Exception is not null) - throw Exception; - } - - public bool IsSuccess => Exception is null; - - public ServiceUnavailableException AsServiceUnavailableException() => - AsException(ServiceUnavailableException.ErrorType); - - public InternalServerErrorException AsInternalServerErrorException() => - AsException(InternalServerErrorException.ErrorType); - - public TransactionCanceledException AsTransactionCanceledException() => - AsException(TransactionCanceledException.ErrorType); - - public ConditionalCheckFailedException AsConditionalCheckFailedException() => - AsException(ConditionalCheckFailedException.ErrorType); - - public ProvisionedThroughputExceededException AsProvisionedThroughputExceededException() => - AsException(ProvisionedThroughputExceededException.ErrorType); - - public AccessDeniedException AsAccessDeniedException() => - AsException(AccessDeniedException.ErrorType); - - public IncompleteSignatureException AsIncompleteSignatureException() => - AsException(IncompleteSignatureException.ErrorType); - - public ItemCollectionSizeLimitExceededException AsItemCollectionSizeLimitExceededException() => - AsException(ItemCollectionSizeLimitExceededException.ErrorType); - - public LimitExceededException AsLimitExceededException() => - AsException(LimitExceededException.ErrorType); - - public MissingAuthenticationTokenException AsMissingAuthenticationTokenException() => - AsException(MissingAuthenticationTokenException.ErrorType); - - public RequestLimitExceededException AsRequestLimitExceededException() => - AsException(RequestLimitExceededException.ErrorType); - - public ResourceInUseException AsResourceInUseException() => - AsException(ResourceInUseException.ErrorType); - - public ResourceNotFoundException AsResourceNotFoundException() => - AsException(ResourceNotFoundException.ErrorType); - - public ThrottlingException AsThrottlingException() => - AsException(ThrottlingException.ErrorType); - - public UnrecognizedClientException AsUnrecognizedClientException() => - AsException(UnrecognizedClientException.ErrorType); - - public ValidationException AsValidationException() => - AsException(ValidationException.ErrorType); - - public IdempotentParameterMismatchException AsIdempotentParameterMismatchException() => - AsException(IdempotentParameterMismatchException.ErrorType); - - public TransactionInProgressException AsTransactionInProgressException() => - AsException(TransactionInProgressException.ErrorType); - - private T AsException(OpErrorType expectedType) where T : DdbException => Exception switch - { - T ex => ex, - _ => throw new InvalidOperationException($"Operation error contains '{ErrorType}' value instead of '{expectedType}'.") - }; - } - - public readonly struct OpResult - { - public T? Value { get; } - - public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; - - public DdbException? Exception { get; } - - public OpResult(DdbException exception) - { - Value = default; - Exception = exception; - } - - public OpResult(T value) - { - Value = value; - Exception = null; - } - - public T EnsureSuccess() - { - if (ErrorType != OpErrorType.None && Exception is not null) - throw Exception; - - return Value!; - } - - public bool IsSuccess => Exception is null; - - public OpResult DiscardValue() => new(Exception); - - public ServiceUnavailableException AsServiceUnavailableException() => - AsException(ServiceUnavailableException.ErrorType); - - public InternalServerErrorException AsInternalServerErrorException() => - AsException(InternalServerErrorException.ErrorType); - - public TransactionCanceledException AsTransactionCanceledException() => - AsException(TransactionCanceledException.ErrorType); - - public ConditionalCheckFailedException AsConditionalCheckFailedException() => - AsException(ConditionalCheckFailedException.ErrorType); - - public ProvisionedThroughputExceededException AsProvisionedThroughputExceededException() => - AsException(ProvisionedThroughputExceededException.ErrorType); - - public AccessDeniedException AsAccessDeniedException() => - AsException(AccessDeniedException.ErrorType); - - public IncompleteSignatureException AsIncompleteSignatureException() => - AsException(IncompleteSignatureException.ErrorType); - - public ItemCollectionSizeLimitExceededException AsItemCollectionSizeLimitExceededException() => - AsException(ItemCollectionSizeLimitExceededException.ErrorType); - - public LimitExceededException AsLimitExceededException() => - AsException(LimitExceededException.ErrorType); - - public MissingAuthenticationTokenException AsMissingAuthenticationTokenException() => - AsException(MissingAuthenticationTokenException.ErrorType); - - public RequestLimitExceededException AsRequestLimitExceededException() => - AsException(RequestLimitExceededException.ErrorType); - - public ResourceInUseException AsResourceInUseException() => - AsException(ResourceInUseException.ErrorType); - - public ResourceNotFoundException AsResourceNotFoundException() => - AsException(ResourceNotFoundException.ErrorType); - - public ThrottlingException AsThrottlingException() => - AsException(ThrottlingException.ErrorType); - - public UnrecognizedClientException AsUnrecognizedClientException() => - AsException(UnrecognizedClientException.ErrorType); - - public ValidationException AsValidationException() => - AsException(ValidationException.ErrorType); - - public IdempotentParameterMismatchException AsIdempotentParameterMismatchException() => - AsException(IdempotentParameterMismatchException.ErrorType); - - public TransactionInProgressException AsTransactionInProgressException() => - AsException(TransactionInProgressException.ErrorType); - - private TException AsException(OpErrorType expectedType) where TException : DdbException => Exception switch - { - TException ex => ex, - _ => throw new InvalidOperationException($"Operation error contains '{ErrorType}' value instead of '{expectedType}'.") - }; - } - - internal static class OpResultExtensions - { - public static async Task EnsureSuccess(this Task> task) - { - var result = await task.ConfigureAwait(false); - return result.EnsureSuccess(); - } - - public static async Task EnsureSuccess(this Task task) - { - var result = await task.ConfigureAwait(false); - result.EnsureSuccess(); - } - } - - public enum OpErrorType - { - None, - Unknown, - ServiceUnavailable, - InternalServerError, - TransactionCanceled, - ConditionalCheckFailed, - ProvisionedThroughputExceeded, - AccessDenied, - IncompleteSignature, - ItemCollectionSizeLimitExceeded, - LimitExceeded, - MissingAuthenticationToken, - RequestLimitExceeded, - ResourceInUse, - ResourceNotFound, - Throttling, - UnrecognizedClient, - Validation, - IdempotentParameterMismatch, - TransactionInProgress - } -} \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/ResultHandling/OpErrorType.cs b/src/EfficientDynamoDb/Operations/ResultHandling/OpErrorType.cs new file mode 100644 index 00000000..44205819 --- /dev/null +++ b/src/EfficientDynamoDb/Operations/ResultHandling/OpErrorType.cs @@ -0,0 +1,25 @@ +namespace EfficientDynamoDb.Operations; + +public enum OpErrorType +{ + None, + Unknown, + ServiceUnavailable, + InternalServerError, + TransactionCanceled, + ConditionalCheckFailed, + ProvisionedThroughputExceeded, + AccessDenied, + IncompleteSignature, + ItemCollectionSizeLimitExceeded, + LimitExceeded, + MissingAuthenticationToken, + RequestLimitExceeded, + ResourceInUse, + ResourceNotFound, + Throttling, + UnrecognizedClient, + Validation, + IdempotentParameterMismatch, + TransactionInProgress +} \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs b/src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs new file mode 100644 index 00000000..2eaab71d --- /dev/null +++ b/src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs @@ -0,0 +1,72 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using EfficientDynamoDb.Exceptions; + +namespace EfficientDynamoDb.Operations; + +public interface IOpResult +{ + public OpErrorType ErrorType { get; } + + public DdbException? Exception { get; } + + [MemberNotNullWhen(false, nameof(Exception))] + public bool IsSuccess { get; } +} + +[StructLayout(LayoutKind.Auto)] +public readonly struct OpResult : IOpResult +{ + public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; + + public DdbException? Exception { get; } + + public OpResult(DdbException? exception) + { + Exception = exception; + } + + public void EnsureSuccess() + { + if (Exception is not null) + throw Exception; + } + + [MemberNotNullWhen(false, nameof(Exception))] + public bool IsSuccess => Exception is null; +} + +public readonly struct OpResult : IOpResult +{ + public T? Value { get; } + + public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; + + public DdbException? Exception { get; } + + public OpResult(DdbException exception) + { + Value = default; + Exception = exception; + } + + public OpResult(T value) + { + Value = value; + Exception = null; + } + + [MemberNotNullWhen(true, nameof(Value))] + public T EnsureSuccess() + { + if (ErrorType != OpErrorType.None && Exception is not null) + throw Exception; + + return Value!; + } + + [MemberNotNullWhen(false, nameof(Exception))] + public bool IsSuccess => Exception is null; + + public OpResult DiscardValue() => new(Exception); +} \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/ResultHandling/OpResultExtensions.cs b/src/EfficientDynamoDb/Operations/ResultHandling/OpResultExtensions.cs new file mode 100644 index 00000000..19d114bd --- /dev/null +++ b/src/EfficientDynamoDb/Operations/ResultHandling/OpResultExtensions.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using EfficientDynamoDb.Exceptions; + +namespace EfficientDynamoDb.Operations; + +public static class OpResultExtensions +{ + public static ServiceUnavailableException AsServiceUnavailableException(this T opResult) where T : IOpResult => + AsException(opResult, ServiceUnavailableException.ErrorType); + + public static InternalServerErrorException AsInternalServerErrorException(this T opResult) where T : IOpResult => + AsException(opResult, InternalServerErrorException.ErrorType); + + public static TransactionCanceledException AsTransactionCanceledException(this T opResult) where T : IOpResult => + AsException(opResult, TransactionCanceledException.ErrorType); + + public static ConditionalCheckFailedException AsConditionalCheckFailedException(this T opResult) where T : IOpResult => + AsException(opResult, ConditionalCheckFailedException.ErrorType); + + public static ProvisionedThroughputExceededException AsProvisionedThroughputExceededException(this T opResult) where T : IOpResult => + AsException(opResult, ProvisionedThroughputExceededException.ErrorType); + + public static AccessDeniedException AsAccessDeniedException(this T opResult) where T : IOpResult => + AsException(opResult, AccessDeniedException.ErrorType); + + public static IncompleteSignatureException AsIncompleteSignatureException(this T opResult) where T : IOpResult => + AsException(opResult, IncompleteSignatureException.ErrorType); + + public static ItemCollectionSizeLimitExceededException AsItemCollectionSizeLimitExceededException(this T opResult) where T : IOpResult => + AsException(opResult, ItemCollectionSizeLimitExceededException.ErrorType); + + public static LimitExceededException AsLimitExceededException(this T opResult) where T : IOpResult => + AsException(opResult, LimitExceededException.ErrorType); + + public static MissingAuthenticationTokenException AsMissingAuthenticationTokenException(this T opResult) where T : IOpResult => + AsException(opResult, MissingAuthenticationTokenException.ErrorType); + + public static RequestLimitExceededException AsRequestLimitExceededException(this T opResult) where T : IOpResult => + AsException(opResult, RequestLimitExceededException.ErrorType); + + public static ResourceInUseException AsResourceInUseException(this T opResult) where T : IOpResult => + AsException(opResult, ResourceInUseException.ErrorType); + + public static ResourceNotFoundException AsResourceNotFoundException(this T opResult) where T : IOpResult => + AsException(opResult, ResourceNotFoundException.ErrorType); + + public static ThrottlingException AsThrottlingException(this T opResult) where T : IOpResult => + AsException(opResult, ThrottlingException.ErrorType); + + public static UnrecognizedClientException AsUnrecognizedClientException(this T opResult) where T : IOpResult => + AsException(opResult, UnrecognizedClientException.ErrorType); + + public static ValidationException AsValidationException(this T opResult) where T : IOpResult => + AsException(opResult, ValidationException.ErrorType); + + public static IdempotentParameterMismatchException AsIdempotentParameterMismatchException(this T opResult) where T : IOpResult => + AsException(opResult, IdempotentParameterMismatchException.ErrorType); + + public static TransactionInProgressException AsTransactionInProgressException(this T opResult) where T : IOpResult => + AsException(opResult, TransactionInProgressException.ErrorType); + + private static TException AsException(this T opResult, OpErrorType expectedType) + where TException : DdbException + where T : IOpResult + => opResult.Exception switch + { + TException ex => ex, + _ => throw new InvalidOperationException($"Operation error contains '{opResult.ErrorType}' value instead of '{expectedType}'.") + }; + + internal static async Task EnsureSuccess(this Task> task) + { + var result = await task.ConfigureAwait(false); + return result.EnsureSuccess(); + } + + internal static async Task EnsureSuccess(this Task task) + { + var result = await task.ConfigureAwait(false); + result.EnsureSuccess(); + } +} \ No newline at end of file From f5615a69f403034fa7d20a678a05e0eb5bb7e6c4 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Mon, 23 Jun 2025 01:04:44 +0200 Subject: [PATCH 27/28] Adjust namespaces --- .../{ErrorHandling => ResultHandling}/OpResultShould.cs | 2 +- .../OpResultWithValueShould.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/EfficientDynamoDb.Tests/Operations/{ErrorHandling => ResultHandling}/OpResultShould.cs (98%) rename src/EfficientDynamoDb.Tests/Operations/{ErrorHandling => ResultHandling}/OpResultWithValueShould.cs (98%) diff --git a/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultShould.cs b/src/EfficientDynamoDb.Tests/Operations/ResultHandling/OpResultShould.cs similarity index 98% rename from src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultShould.cs rename to src/EfficientDynamoDb.Tests/Operations/ResultHandling/OpResultShould.cs index 9ed1e9ce..af50c722 100644 --- a/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultShould.cs +++ b/src/EfficientDynamoDb.Tests/Operations/ResultHandling/OpResultShould.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using Shouldly; -namespace EfficientDynamoDb.Tests.Operations.ErrorHandling; +namespace EfficientDynamoDb.Tests.Operations.ResultHandling; [TestFixture] public class OpResultShould diff --git a/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultWithValueShould.cs b/src/EfficientDynamoDb.Tests/Operations/ResultHandling/OpResultWithValueShould.cs similarity index 98% rename from src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultWithValueShould.cs rename to src/EfficientDynamoDb.Tests/Operations/ResultHandling/OpResultWithValueShould.cs index efca39f0..7a6811ca 100644 --- a/src/EfficientDynamoDb.Tests/Operations/ErrorHandling/OpResultWithValueShould.cs +++ b/src/EfficientDynamoDb.Tests/Operations/ResultHandling/OpResultWithValueShould.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using Shouldly; -namespace EfficientDynamoDb.Tests.Operations.ErrorHandling; +namespace EfficientDynamoDb.Tests.Operations.ResultHandling; [TestFixture] public class OpResultWithValueShould From 67557cfe2411eb3d7f64292cc41ec9dc0460a109 Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Mon, 23 Jun 2025 04:33:17 +0200 Subject: [PATCH 28/28] XML docs for OpResult --- .../Operations/ResultHandling/OpResult.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs b/src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs index 2eaab71d..bd285f61 100644 --- a/src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs +++ b/src/EfficientDynamoDb/Operations/ResultHandling/OpResult.cs @@ -4,58 +4,106 @@ namespace EfficientDynamoDb.Operations; +/// +/// Represents the result of a DynamoDB operation. +/// public interface IOpResult { + /// + /// Gets the type of error that occurred during the operation. + /// public OpErrorType ErrorType { get; } + /// + /// Gets the exception that occurred during the operation, if any. + /// public DdbException? Exception { get; } + /// + /// Gets a value indicating whether the operation was successful. + /// [MemberNotNullWhen(false, nameof(Exception))] public bool IsSuccess { get; } } +/// +/// Represents the result of a DynamoDB operation. +/// [StructLayout(LayoutKind.Auto)] public readonly struct OpResult : IOpResult { + /// public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; + /// public DdbException? Exception { get; } + /// + /// Initializes a new instance of the struct. + /// + /// The exception that occurred during the operation. public OpResult(DdbException? exception) { Exception = exception; } + /// + /// Throws an exception if the operation was not successful. + /// + /// The exception that occurred during the operation. public void EnsureSuccess() { if (Exception is not null) throw Exception; } + /// [MemberNotNullWhen(false, nameof(Exception))] public bool IsSuccess => Exception is null; } +/// +/// Represents the result of a DynamoDB operation with a value. +/// +/// The type of the value. public readonly struct OpResult : IOpResult { + /// + /// Gets the actual value of the operation result if it was successful, otherwise the default value of . + /// public T? Value { get; } + /// public OpErrorType ErrorType => Exception?.OpErrorType ?? OpErrorType.None; + /// public DdbException? Exception { get; } + /// + /// Initializes a new instance of the struct with an exception. + /// + /// The exception that occurred during the operation. public OpResult(DdbException exception) { Value = default; Exception = exception; } + /// + /// Initializes a new instance of the struct with a value. + /// + /// The value of the operation result. public OpResult(T value) { Value = value; Exception = null; } + /// + /// Throws an exception if the operation was not successful, otherwise returns the value. + /// + /// The value of the operation result. + /// The exception that occurred during the operation. [MemberNotNullWhen(true, nameof(Value))] public T EnsureSuccess() { @@ -65,8 +113,13 @@ public T EnsureSuccess() return Value!; } + /// [MemberNotNullWhen(false, nameof(Exception))] public bool IsSuccess => Exception is null; + /// + /// Creates a new from the current instance, discarding the value. + /// + /// A new instance. public OpResult DiscardValue() => new(Exception); } \ No newline at end of file