diff --git a/generator/.DevConfigs/9490947f-209f-47e9-8c70-3698872df304.json b/generator/.DevConfigs/9490947f-209f-47e9-8c70-3698872df304.json
new file mode 100644
index 000000000000..5565c81319d5
--- /dev/null
+++ b/generator/.DevConfigs/9490947f-209f-47e9-8c70-3698872df304.json
@@ -0,0 +1,11 @@
+{
+ "services": [
+ {
+ "serviceName": "DynamoDBv2",
+ "type": "minor",
+ "changeLogMessages": [
+ "Add Request Object Pattern and Expression-Based for DynamoDB Document Model."
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentOperationPipeline.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentOperationPipeline.cs
new file mode 100644
index 000000000000..0d188ac281be
--- /dev/null
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentOperationPipeline.cs
@@ -0,0 +1,434 @@
+using Amazon.DynamoDBv2.Model;
+using Amazon.Runtime;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Amazon.DynamoDBv2.DocumentModel
+{
+ ///
+ /// Provides an abstract pipeline for executing document operations against a DynamoDB table, supporting both
+ /// synchronous and asynchronous execution patterns.
+ ///
+ /// This class defines the structure for mapping high-level document requests to low-level
+ /// DynamoDB requests, validating input, applying expressions, invoking the service, and post-processing results.
+ /// Derived classes must implement the abstract methods to provide specific operation logic. The pipeline ensures
+ /// that requests are executed with the appropriate user agent details and supports cancellation for asynchronous
+ /// operations.
+ /// The type representing the high-level request for the document operation.
+ /// The type representing the low-level DynamoDB request. Must inherit from AmazonDynamoDBRequest.
+ /// The type representing the response from the DynamoDB service. Must inherit from AmazonWebServiceResponse.
+ /// The type representing the final result produced by the pipeline after processing the service response.
+ internal abstract class DocumentOperationPipeline
+ where TLowLevelRequest : AmazonDynamoDBRequest
+ where TServiceResponse : AmazonWebServiceResponse
+ {
+ protected readonly Table Table;
+
+ protected DocumentOperationPipeline(Table table)
+ {
+ Table = table ?? throw new ArgumentNullException(nameof(table));
+ }
+
+ ///
+ /// Executes the specified high-level request synchronously and returns the processed result.
+ ///
+ /// The high-level request to execute. Cannot be null.
+ /// The result of processing the request, of type .
+ public TResult ExecuteSync(THighLevelRequest request)
+ {
+ Validate(request);
+ var low = Map(request);
+ ApplyExpressions(request, low);
+ var resp = InvokeSync(low);
+ return PostProcess(request, resp);
+ }
+
+ ///
+ /// Executes the specified high-level request asynchronously and returns the processed result.
+ ///
+ /// The high-level request to execute. Cannot be null.
+ /// A cancellation token that can be used to cancel the operation.
+ /// A task that represents the asynchronous operation. The task result contains the processed response for the
+ /// specified request.
+ public async Task ExecuteAsync(THighLevelRequest request, CancellationToken ct)
+ {
+ Validate(request);
+ var low = Map(request);
+ Table.UpdateRequestUserAgentDetails(low, isAsync: true);
+ ApplyExpressions(request, low);
+ var resp = await InvokeAsync(low, ct).ConfigureAwait(false);
+ return PostProcess(request, resp);
+ }
+
+ ///
+ /// Validates the specified high-level request to ensure it meets all required criteria before processing.
+ ///
+ /// The high-level request to validate. Cannot be null.
+ protected abstract void Validate(THighLevelRequest request);
+
+ ///
+ /// Converts a high-level request object to its corresponding low-level request representation.
+ ///
+ /// The high-level request to be mapped. Cannot be null.
+ /// The low-level request object that represents the mapped version of the specified high-level request.
+ protected abstract TLowLevelRequest Map(THighLevelRequest request);
+
+ ///
+ /// Applies high-level request parameters to the specified low-level request object.
+ ///
+ /// Implementations should ensure that all relevant parameters from the high-level
+ /// request are correctly mapped to the low-level request. This method does not perform validation; callers are
+ /// responsible for providing valid request objects.
+ /// The high-level request containing parameters and options to be applied.
+ /// The low-level request object that will be modified based on the high-level request.
+ protected abstract void ApplyExpressions(THighLevelRequest request, TLowLevelRequest lowLevel);
+
+ ///
+ /// Invokes the low-level request synchronously against the DynamoDB service.
+ ///
+ /// The low-level request object to be sent to the service. Cannot be null.
+ /// The service response returned by the service.
+ protected abstract TServiceResponse InvokeSync(TLowLevelRequest lowLevel);
+
+ ///
+ /// Asynchronously sends a low-level request and returns the corresponding service response.
+ ///
+ /// The low-level request object to be sent to the service. Cannot be null.
+ /// A cancellation token that can be used to cancel the operation.
+ /// A task that represents the asynchronous operation. The task result contains the service response returned by
+ /// the service.
+ protected abstract Task InvokeAsync(TLowLevelRequest lowLevel, CancellationToken ct);
+
+ ///
+ /// Performs the final transformation of the raw DynamoDB service response into the high-level
+ /// result type returned to the caller.
+ ///
+ ///
+ /// The original high-level request that initiated the operation. Guaranteed non-null (already validated
+ /// by ).
+ ///
+ ///
+ /// The low-level response object returned by the DynamoDB service invocation. Contains the raw data
+ /// (e.g., attribute maps) needed to construct the final result.
+ ///
+ ///
+ /// The processed result derived from . Implementations may return
+ /// null for operations whose semantics do not yield a material result (e.g., PutItem without requesting
+ /// return values).
+ ///
+ ///
+ /// Typical responsibilities of an implementation include:
+ /// 1. Committing or reconciling state changes on the request (e.g., persisting Document mutations).
+ /// 2. Converting attribute dictionaries to instances or other domain types.
+ /// 3. Extracting and shaping operation metadata when required by higher-level abstractions.
+ ///
+ /// Implementations should avoid throwing unless an unexpected serialization or mapping issue occurs.
+ /// Side effects should be limited to intentional state commits on objects referenced by .
+ ///
+ protected abstract TResult PostProcess(THighLevelRequest request, TServiceResponse serviceResponse);
+ }
+
+ ///
+ /// Provides a pipeline for executing document-based PutItem operations against a DynamoDB table.
+ ///
+ /// This class maps high-level document requests to low-level PutItem requests, applies
+ /// conditional expressions, and processes responses to support document-oriented workflows. It is intended for
+ /// internal use within the document API infrastructure and is not designed for direct consumption by application
+ /// code.
+ internal sealed class PutItemPipeline : DocumentOperationPipeline
+ {
+ public PutItemPipeline(Table table) : base(table)
+ {
+ }
+
+ protected override void Validate(PutItemDocumentOperationRequest request)
+ {
+ if (request == null)
+ throw new ArgumentNullException(nameof(request));
+ if (request.Document == null)
+ throw new InvalidOperationException(
+ "The Document property of the PutItemDocumentOperationRequest cannot be null.");
+ }
+
+ protected override PutItemRequest Map(PutItemDocumentOperationRequest request)
+ {
+ var req = new PutItemRequest
+ {
+ TableName = Table.TableName,
+ Item = Table.ToAttributeMap(request.Document)
+ };
+ if (request.ReturnValues == ReturnValues.AllOldAttributes)
+ req.ReturnValues = EnumMapper.Convert(request.ReturnValues);
+ return req;
+ }
+
+ protected override void ApplyExpressions(PutItemDocumentOperationRequest request, PutItemRequest lowLevel)
+ {
+ if (request.ConditionalExpression is { IsSet: true })
+ request.ConditionalExpression.ApplyExpression(lowLevel, Table);
+ }
+
+ protected override PutItemResponse InvokeSync(PutItemRequest lowLevel)
+ {
+#if NETSTANDARD
+
+ // Cast the IAmazonDynamoDB to the concrete client instead, so we can access the internal sync-over-async methods
+ var client = Table.DDBClient as AmazonDynamoDBClient;
+ if (client == null)
+ {
+ throw new InvalidOperationException("Calling the synchronous PutItem from .NET or .NET Core requires initializing the Table " +
+ "with an actual AmazonDynamoDBClient. You can use a mocked or substitute IAmazonDynamoDB when creating a Table via PutItemAsync instead.");
+ }
+ return client.PutItem(lowLevel);
+#else
+ return Table.DDBClient.PutItem(lowLevel);
+#endif
+ }
+
+
+ protected override Task InvokeAsync(PutItemRequest lowLevel, CancellationToken ct)
+ => Table.DDBClient.PutItemAsync(lowLevel, ct);
+
+ protected override Document PostProcess(PutItemDocumentOperationRequest request,
+ PutItemResponse serviceResponse)
+ {
+ request.Document.CommitChanges();
+ if (request.ReturnValues == ReturnValues.AllOldAttributes && serviceResponse.Attributes != null)
+ return Table.FromAttributeMap(serviceResponse.Attributes);
+ return null;
+ }
+ }
+
+ ///
+ /// Provides a pipeline for executing a DynamoDB GetItem operation using document-based requests and responses.
+ ///
+ /// This class maps high-level document operation requests to low-level GetItem requests, applies
+ /// projection expressions, and processes responses into document objects. It is intended for internal use within
+ /// the document operation framework.
+ internal sealed class GetItemPipeline
+ : DocumentOperationPipeline
+ {
+ public GetItemPipeline(Table table) : base(table)
+ {
+ }
+
+ protected override void Validate(GetItemDocumentOperationRequest request)
+ {
+ if (request == null) throw new ArgumentNullException(nameof(request));
+
+ if (request.Key == null || request.Key.Count == 0)
+ throw new InvalidOperationException("GetItemDocumentOperationRequest.Key cannot be null or empty.");
+ }
+
+ protected override GetItemRequest Map(GetItemDocumentOperationRequest request)
+ {
+ return new GetItemRequest
+ {
+ TableName = Table.TableName,
+ Key = Table.MakeKey(request.Key),
+ ConsistentRead = request.ConsistentRead
+ };
+ }
+
+ protected override void ApplyExpressions(GetItemDocumentOperationRequest request, GetItemRequest lowLevel)
+ {
+ if (request.ProjectionExpression is { IsSet: true })
+ request.ProjectionExpression.ApplyExpression(lowLevel, Table);
+ }
+
+ protected override GetItemResponse InvokeSync(GetItemRequest lowLevel)
+ {
+#if NETSTANDARD
+ // Cast the IAmazonDynamoDB to the concrete client instead, so we can access the internal sync-over-async methods
+ var client = Table.DDBClient as AmazonDynamoDBClient;
+ if (client == null)
+ {
+ throw new InvalidOperationException("Calling the synchronous GetItem from .NET or .NET Core requires initializing the Table " +
+ "with an actual AmazonDynamoDBClient. You can use a mocked or substitute IAmazonDynamoDB when creating a Table via GetItemAsync instead.");
+ }
+ return client.GetItem(lowLevel);
+#else
+ return Table.DDBClient.GetItem(lowLevel);
+#endif
+ }
+
+
+ protected override async Task InvokeAsync(GetItemRequest lowLevel, CancellationToken ct) =>
+ await Table.DDBClient.GetItemAsync(lowLevel, ct).ConfigureAwait(false);
+
+ protected override Document PostProcess(GetItemDocumentOperationRequest request, GetItemResponse serviceResponse)
+ {
+ var map = serviceResponse.Item;
+ return (map == null || map.Count == 0) ? null : Table.FromAttributeMap(map);
+ }
+ }
+
+ ///
+ /// Provides a pipeline for processing update operations on DynamoDB items, handling validation, mapping, and
+ /// execution of update requests.
+ ///
+ /// This class is intended for internal use within the document model infrastructure and
+ /// coordinates the conversion of high-level update requests into low-level DynamoDB update operations. It ensures
+ /// that either a document or an update expression is provided exclusively, and manages the mapping of keys and
+ /// update expressions.
+ internal sealed class UpdateItemPipeline :
+ DocumentOperationPipeline
+ {
+ public UpdateItemPipeline(Table table) : base(table)
+ {
+ }
+
+ protected override void Validate(UpdateItemDocumentOperationRequest request)
+ {
+ if (request == null) throw new ArgumentNullException(nameof(request));
+ bool docSet = request.Document != null;
+ bool exprSet = request.UpdateExpression is { IsSet: true };
+ if ((docSet && exprSet) || (!docSet && !exprSet))
+ throw new InvalidOperationException("Either Document or UpdateExpression must be set (exclusively).");
+ }
+
+ protected override UpdateItemRequest Map(UpdateItemDocumentOperationRequest request)
+ {
+ var req = new UpdateItemRequest
+ {
+ TableName = Table.TableName,
+ ReturnValues = EnumMapper.Convert(request.ReturnValues)
+ };
+
+ Key key = request.Key != null ? Table.MakeKey(request.Key) : null;
+
+ if (request.Document != null)
+ {
+ key ??= Table.MakeKey(request.Document);
+ bool haveKeysChanged = Table.HaveKeysChanged(request.Document);
+ var attrUpdates = Table.ToAttributeUpdateMap(request.Document, !haveKeysChanged);
+ foreach (var k in Table.KeyNames)
+ attrUpdates.Remove(k);
+
+ Common.ConvertAttributeUpdatesToUpdateExpression(
+ attrUpdates,
+ request.UpdateExpression,
+ Table,
+ out var statement,
+ out var exprValues,
+ out var exprNames);
+
+ req.UpdateExpression = statement;
+ req.ExpressionAttributeValues = exprValues;
+ req.ExpressionAttributeNames = exprNames;
+ }
+
+ if (key == null || key.Count == 0)
+ throw new InvalidOperationException("UpdateItem requires a key either via request.Key or Document.");
+
+ req.Key = key;
+
+ return req;
+ }
+
+ protected override void ApplyExpressions(UpdateItemDocumentOperationRequest request, UpdateItemRequest lowLevel)
+ {
+ if (request.UpdateExpression is { IsSet: true })
+ request.UpdateExpression.ApplyUpdateExpression(lowLevel, Table);
+ if (request.ConditionalExpression is { IsSet: true })
+ request.ConditionalExpression.ApplyConditionalExpression(lowLevel, Table);
+ }
+
+ protected override UpdateItemResponse InvokeSync(UpdateItemRequest lowLevel)
+ {
+#if NETSTANDARD
+ // Cast the IAmazonDynamoDB to the concrete client instead, so we can access the internal sync-over-async methods
+ var client = Table.DDBClient as AmazonDynamoDBClient;
+ if (client == null)
+ {
+ throw new InvalidOperationException("Calling the synchronous GetItem from .NET or .NET Core requires initializing the Table " +
+ "with an actual AmazonDynamoDBClient. You can use a mocked or substitute IAmazonDynamoDB when creating a Table via UpdateItemAsync instead.");
+ }
+ return client.UpdateItem(lowLevel);
+#else
+ return Table.DDBClient.UpdateItem(lowLevel);
+#endif
+ }
+
+ protected override async Task InvokeAsync(UpdateItemRequest lowLevel, CancellationToken ct) =>
+ await Table.DDBClient.UpdateItemAsync(lowLevel, ct).ConfigureAwait(false);
+
+ protected override Document PostProcess(UpdateItemDocumentOperationRequest request,
+ UpdateItemResponse serviceResponse)
+ {
+ var resp = (UpdateItemResponse)serviceResponse;
+ request.Document?.CommitChanges();
+ if (request.ReturnValues != ReturnValues.None && resp.Attributes != null)
+ return Table.FromAttributeMap(resp.Attributes);
+ return null;
+ }
+ }
+
+ ///
+ /// Provides a pipeline for executing a DynamoDB DeleteItem operation using document model requests and responses.
+ ///
+ /// This class maps high-level document-based delete requests to low-level DeleteItem operations
+ /// against a specific DynamoDB table. It handles validation, expression application, synchronous and asynchronous
+ /// invocation, and post-processing of results. The pipeline is intended for internal use within the document model.
+ ///
+ internal sealed class DeleteItemPipeline :
+ DocumentOperationPipeline
+ {
+ public DeleteItemPipeline(Table table) : base(table) { }
+
+ protected override void Validate(DeleteItemDocumentOperationRequest request)
+ {
+ if (request == null) throw new ArgumentNullException(nameof(request));
+ if (request.Key == null || request.Key.Count == 0)
+ throw new InvalidOperationException("DeleteItemDocumentOperationRequest.Key cannot be null or empty.");
+ }
+
+ protected override DeleteItemRequest Map(DeleteItemDocumentOperationRequest request)
+ {
+ var req = new DeleteItemRequest
+ {
+ TableName = Table.TableName,
+ Key = Table.MakeKey(request.Key)
+ };
+ if (request.ReturnValues == ReturnValues.AllOldAttributes)
+ req.ReturnValues = EnumMapper.Convert(request.ReturnValues);
+ return req;
+ }
+
+ protected override void ApplyExpressions(DeleteItemDocumentOperationRequest request, DeleteItemRequest lowLevel)
+ {
+ if (request.ConditionalExpression is { IsSet: true })
+ request.ConditionalExpression.ApplyExpression(lowLevel, Table);
+ }
+
+ protected override DeleteItemResponse InvokeSync(DeleteItemRequest lowLevel)
+ {
+#if NETSTANDARD
+ // Cast the IAmazonDynamoDB to the concrete client instead, so we can access the internal sync-over-async methods
+ var client = Table.DDBClient as AmazonDynamoDBClient;
+ if (client == null)
+ {
+ throw new InvalidOperationException("Calling the synchronous GetItem from .NET or .NET Core requires initializing the Table " +
+ "with an actual AmazonDynamoDBClient. You can use a mocked or substitute IAmazonDynamoDB when creating a Table via DeleteItemAsync instead.");
+ }
+ return client.DeleteItem(lowLevel);
+#else
+ return Table.DDBClient.DeleteItem(lowLevel);
+#endif
+ }
+
+ protected override async Task InvokeAsync(DeleteItemRequest lowLevel, CancellationToken ct) =>
+ await Table.DDBClient.DeleteItemAsync(lowLevel, ct).ConfigureAwait(false);
+
+
+ protected override Document PostProcess(DeleteItemDocumentOperationRequest request, DeleteItemResponse serviceResponse)
+ {
+ var resp = (DeleteItemResponse)serviceResponse;
+ if (request.ReturnValues == ReturnValues.AllOldAttributes && resp.Attributes != null)
+ return Table.FromAttributeMap(resp.Attributes);
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentOperationRequest.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentOperationRequest.cs
index 967e2beed87d..b0f587e5556f 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentOperationRequest.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/DocumentOperationRequest.cs
@@ -12,6 +12,32 @@ public abstract class DocumentOperationRequest
{
}
+ ///
+ /// Represents a request to get a single item from a DynamoDB table using the Document Model.
+ /// This class introduces a modern expression-based API that replaces legacy parameter-based approaches.
+ /// Legacy parameters such as AttributesToGet are not supported. Use ProjectionExpression instead.
+ ///
+ public class GetItemDocumentOperationRequest : DocumentOperationRequest
+ {
+ ///
+ /// Gets or sets the key identifying the item in the table.
+ /// All key components (partition and sort key, if the table has one) must be provided.
+ ///
+ public IDictionary Key { get; set; }
+
+ ///
+ /// Gets or sets the projection expression specifying which attributes should be retrieved.
+ /// If null, all attributes are returned.
+ ///
+ public Expression ProjectionExpression { get; set; }
+
+ ///
+ /// Gets or sets the consistent read flag.
+ /// Strongly consistent reads are only valid for tables and local secondary indexes.
+ ///
+ public bool ConsistentRead { get; set; }
+ }
+
///
/// Represents a request to query items in a DynamoDB table using the Document Model.
/// This class introduces a modern expression-based API that replaces legacy parameter-based approaches.
@@ -83,4 +109,161 @@ public QueryDocumentOperationRequest()
///
public string PaginationToken { get; set; }
}
+
+ ///
+ /// Represents a request to scan items in a DynamoDB table or index using the Document Model.
+ /// This class introduces a modern expression-based API that replaces legacy parameter-based approaches.
+ /// Legacy parameters such as ScanFilter, AttributesToGet are not supported.
+ /// Use FilterExpression and ProjectionExpression instead.
+ ///
+ public class ScanDocumentOperationRequest : DocumentOperationRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ScanDocumentOperationRequest()
+ {
+ Limit = Int32.MaxValue;
+ Select = SelectValues.AllAttributes;
+ TotalSegments = 1;
+ }
+
+ ///
+ /// Gets or sets the filter expression specifying which items should be returned after scanning.
+ /// Applied after DynamoDB reads the items.
+ ///
+ public Expression FilterExpression { get; set; }
+
+ ///
+ /// Gets or sets the projection expression specifying which attributes should be retrieved.
+ ///
+ public Expression ProjectionExpression { get; set; }
+
+ ///
+ /// Enum specifying what data to return from the scan (e.g., attributes or count).
+ ///
+ public SelectValues Select { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of items DynamoDB should process before returning a page of results.
+ /// Defaults to int.MaxValue (service default paging behavior).
+ ///
+ public int Limit { get; set; }
+
+ ///
+ /// Gets or sets the consistent read flag. Strongly consistent reads are only supported for tables and local secondary indexes.
+ ///
+ public bool ConsistentRead { get; set; }
+
+ ///
+ /// Gets or sets the index name to scan against.
+ ///
+ public string IndexName { get; set; }
+
+ ///
+ /// Whether to collect GetNextSet and GetRemaining results in Matches property.
+ /// Default is true. If set to false, Matches will always be empty.
+ ///
+ public bool CollectResults { get; set; }
+
+ ///
+ /// Pagination token corresponding to the item where the last Scan operation stopped.
+ /// Set this value to resume the scan from the next item. Retrieved from a Search object.
+ ///
+ public string PaginationToken { get; set; }
+
+ ///
+ /// The segment number to scan in a parallel scan. Must be between 0 and TotalSegments - 1 when specified.
+ /// If null, a non-parallel (single segment) scan is performed.
+ ///
+ public int Segment { get; set; }
+
+ ///
+ /// Total number of segments for a parallel scan. Defaults to 1 (no parallelism).
+ ///
+ public int TotalSegments { get; set; }
+ }
+
+ ///
+ /// Represents a request to update an item in a DynamoDB table using the Document Model.
+ /// This class introduces a modern expression-based API that replaces legacy parameter-based approaches.
+ /// Legacy parameters such as AttributeUpdates, Expected are not supported.
+ /// Use UpdateExpression and ConditionalExpression instead.
+ ///
+ public class UpdateItemDocumentOperationRequest : DocumentOperationRequest
+ {
+ ///
+ /// Gets or sets the key identifying the item in the table.
+ ///
+ public IDictionary Key { get; set; }
+
+ ///
+ /// Gets or sets the attributes to be updated in the item.
+ ///
+ public Document Document { get; set; }
+
+ ///
+ /// Gets or sets the update expression specifying how attributes should be updated.
+ ///
+ public Expression UpdateExpression { get; set; }
+
+ ///
+ /// Gets or sets the conditional expression evaluated before the update is performed.
+ /// If false, a ConditionalCheckFailedException is thrown.
+ ///
+ public Expression ConditionalExpression { get; set; }
+
+ ///
+ /// Flag specifying what values should be returned.
+ ///
+ public ReturnValues ReturnValues { get; set; }
+ }
+
+ ///
+ /// Represents a request to delete an item from a DynamoDB table using the Document Model.
+ /// This class introduces a modern expression-based API that replaces legacy parameter-based approaches.
+ /// Legacy parameters such as Expected are not supported.
+ /// Use ConditionalExpression instead.
+ ///
+ public class DeleteItemDocumentOperationRequest : DocumentOperationRequest
+ {
+ ///
+ /// Gets or sets the key identifying the item in the table.
+ ///
+ public IDictionary Key { get; set; }
+
+ ///
+ /// Gets or sets the conditional expression specifying when the item should be deleted.
+ ///
+ public Expression ConditionalExpression { get; set; }
+
+ ///
+ /// Flag specifying what values should be returned.
+ ///
+ public ReturnValues ReturnValues { get; set; }
+ }
+
+ ///
+ /// Represents a request to put (create or replace) an item in a DynamoDB table using the Document Model.
+ /// This class introduces a modern expression-based API that replaces legacy parameter-based approaches.
+ /// Legacy parameters such as Expected are not supported.
+ /// Use ConditionalExpression instead.
+ ///
+ public class PutItemDocumentOperationRequest : DocumentOperationRequest
+ {
+ ///
+ /// Gets or sets the document to be put in the table.
+ ///
+ public Document Document { get; set; }
+
+ ///
+ /// Gets or sets the conditional expression specifying when the item should be put.
+ ///
+ public Expression ConditionalExpression { get; set; }
+
+ ///
+ /// Flag specifying what values should be returned.
+ ///
+ public ReturnValues ReturnValues { get; set; }
+ }
}
\ No newline at end of file
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs
index 3844463f4d54..745ea740e54f 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Expression.cs
@@ -157,6 +157,47 @@ internal void ApplyExpression(UpdateItemRequest request, Table table)
}
}
+ internal void ApplyConditionalExpression(UpdateItemRequest request, Table table)
+ {
+ request.ConditionExpression = this.ExpressionStatement;
+ MergeAttributes(request, table);
+ }
+
+ internal void ApplyUpdateExpression(UpdateItemRequest request, Table table)
+ {
+ request.UpdateExpression = this.ExpressionStatement;
+ MergeAttributes(request, table);
+ }
+
+ private void MergeAttributes(UpdateItemRequest request, Table table)
+ {
+ var convertToAttributeValues = ConvertToAttributeValues(this.ExpressionAttributeValues, table);
+ request.ExpressionAttributeValues ??= new Dictionary(StringComparer.Ordinal);
+ foreach (var kvp in convertToAttributeValues)
+ {
+ request.ExpressionAttributeValues[kvp.Key] = kvp.Value;
+ }
+
+
+ if (this.ExpressionAttributeNames?.Count > 0)
+ {
+ request.ExpressionAttributeNames ??= new Dictionary(StringComparer.Ordinal);
+ foreach (var kvp in this.ExpressionAttributeNames)
+ {
+ if (!request.ExpressionAttributeNames.ContainsKey(kvp.Key))
+ {
+ request.ExpressionAttributeNames[kvp.Key] = kvp.Value;
+ }
+ }
+ }
+ }
+
+ internal void ApplyExpression(GetItemRequest request, Table table)
+ {
+ request.ProjectionExpression = ExpressionStatement;
+ request.ExpressionAttributeNames = new Dictionary(this.ExpressionAttributeNames);
+ }
+
internal void ApplyExpression(Get request, Table table)
{
request.ProjectionExpression = ExpressionStatement;
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs
index c66b09649997..ec167d855d40 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Table.cs
@@ -13,20 +13,18 @@
* permissions and limitations under the License.
*/
-using System;
-
+using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.Model;
-using Amazon.Runtime;
-using Amazon.Util;
+using Amazon.Runtime.Internal.UserAgent;
+using Amazon.Runtime.Internal.Util;
+using Amazon.Runtime.Telemetry.Tracing;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
+using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
-using System.Collections.Generic;
-using System.Globalization;
-using Amazon.DynamoDBv2.DataModel;
-using Amazon.Runtime.Internal.Util;
-using Amazon.Runtime.Telemetry.Tracing;
-using Amazon.Runtime.Internal.UserAgent;
namespace Amazon.DynamoDBv2.DocumentModel
{
@@ -151,6 +149,15 @@ public partial interface ITable
/// Resultant Search container.
ISearch Scan(ScanOperationConfig config);
+ ///
+ /// Initiates a scan operation on the specified document and returns a search result representing the scan
+ /// outcome.
+ ///
+ /// An object containing the parameters and data required to perform the scan operation. Cannot be null.
+ /// An ISearch instance representing the result of the scan operation. The returned object contains information
+ /// about matches found in the document.
+ ISearch Scan(ScanDocumentOperationRequest operationRequest);
+
#endregion
#region Query
@@ -1302,6 +1309,18 @@ internal async Task PutItemHelperAsync(Document doc, PutItemOperationC
return ret;
}
+ internal async Task PutItemHelperAsync(PutItemDocumentOperationRequest request, CancellationToken cancellationToken)
+ {
+ var pipeline = new PutItemPipeline(this);
+ return await pipeline.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+
+ internal Document PutItemHelper(PutItemDocumentOperationRequest request)
+ {
+ var pipeline = new PutItemPipeline(this);
+ return pipeline.ExecuteSync(request);
+ }
+
#endregion
@@ -1363,6 +1382,18 @@ internal async Task GetItemHelperAsync(Key key, GetItemOperationConfig
return this.FromAttributeMap(attributeMap);
}
+ internal Document GetItemHelper(GetItemDocumentOperationRequest request)
+ {
+ var pipeline = new GetItemPipeline(this);
+ return pipeline.ExecuteSync(request);
+ }
+
+ internal async Task GetItemHelperAsync(GetItemDocumentOperationRequest request, CancellationToken cancellationToken)
+ {
+ var pipeline = new GetItemPipeline(this);
+ return await pipeline.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+
#endregion
@@ -1380,6 +1411,19 @@ internal Task UpdateHelperAsync(Document doc, Primitive hashKey, Primi
return UpdateHelperAsync(doc, key, config, expression, cancellationToken);
}
+ internal Document UpdateHelper(UpdateItemDocumentOperationRequest request)
+ {
+ var pipeline = new UpdateItemPipeline(this);
+ return pipeline.ExecuteSync(request);
+ }
+
+ internal async Task UpdateHelperAsync(UpdateItemDocumentOperationRequest request,
+ CancellationToken cancellationToken)
+ {
+ var pipeline = new UpdateItemPipeline(this);
+ return await pipeline.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+
internal Document UpdateHelper(Document doc, Key key, UpdateItemOperationConfig config, Expression updateExpression)
{
var currentConfig = config ?? new UpdateItemOperationConfig();
@@ -1622,6 +1666,7 @@ internal Document DeleteHelper(Key key, DeleteItemOperationConfig config)
return ret;
}
+
internal async Task DeleteHelperAsync(Key key, DeleteItemOperationConfig config, CancellationToken cancellationToken)
{
var currentConfig = config ?? new DeleteItemOperationConfig();
@@ -1665,6 +1710,19 @@ internal async Task DeleteHelperAsync(Key key, DeleteItemOperationConf
return ret;
}
+
+ internal Document DeleteHelper(DeleteItemDocumentOperationRequest request)
+ {
+ var pipeline = new DeleteItemPipeline(this);
+ return pipeline.ExecuteSync(request);
+ }
+
+ internal async Task DeleteHelperAsync(DeleteItemDocumentOperationRequest request, CancellationToken cancellationToken)
+ {
+ var pipeline = new DeleteItemPipeline(this);
+ return await pipeline.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+
#endregion
@@ -1718,6 +1776,35 @@ public ISearch Scan(ScanOperationConfig config)
return ret;
}
+ ///
+ public ISearch Scan(ScanDocumentOperationRequest operationRequest)
+ {
+ if (operationRequest == null)
+ throw new ArgumentNullException("operationRequest");
+
+ Search ret = new Search(SearchType.Scan)
+ {
+ SourceTable = this,
+ TableName = TableName,
+ Limit = operationRequest.Limit,
+ FilterExpression = operationRequest.FilterExpression,
+ ProjectionExpression = operationRequest.ProjectionExpression,
+ Select = operationRequest.Select,
+ CollectResults = operationRequest.CollectResults,
+ IndexName = operationRequest.IndexName,
+ IsConsistentRead = operationRequest.ConsistentRead,
+ PaginationToken = operationRequest.PaginationToken
+ };
+
+ if (operationRequest.TotalSegments != 0)
+ {
+ ret.TotalSegments = operationRequest.TotalSegments;
+ ret.Segment = operationRequest.Segment;
+ }
+
+ return ret;
+ }
+
#endregion
@@ -1852,7 +1939,7 @@ public IDocumentTransactWrite CreateTransactWrite()
{
return new DocumentTransactWrite(this);
}
-
#endregion
+
}
}
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs
index f4b8ecccc379..206713f5ca25 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/Util.cs
@@ -330,7 +330,7 @@ public static string Convert(ConditionalOperatorValues value)
internal static class Common
{
private const string AwsVariablePrefix = "awsavar";
-
+
public static void ConvertAttributeUpdatesToUpdateExpression(
Dictionary attributesToUpdates, Expression updateExpression,
Table table,
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs
index 8b8abf7110ad..9ed96daf7d9a 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_async/Table.Async.cs
@@ -24,6 +24,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
+using Amazon.DynamoDBv2.DataModel;
using Amazon.Runtime.Internal;
using Amazon.Runtime.Telemetry.Tracing;
@@ -35,7 +36,7 @@ public partial interface ITable
///
/// Initiates the asynchronous execution of the PutItem operation.
- ///
+ ///
///
/// Document to save.
/// Token which can be used to cancel the task.
@@ -44,7 +45,7 @@ public partial interface ITable
///
/// Initiates the asynchronous execution of the PutItem operation.
- ///
+ ///
///
/// Document to save.
/// Configuration to use.
@@ -52,6 +53,17 @@ public partial interface ITable
/// A Task that can be used to poll or wait for results, or both.
Task PutItemAsync(Document doc, PutItemOperationConfig config, CancellationToken cancellationToken = default(CancellationToken));
+ ///
+ /// Initiates the asynchronous execution of the PutItem operation.
+ ///
+ ///
+ /// The PutItemDocumentOperationRequest object containing all parameters for the PutItem operation.
+ /// Token which can be used to cancel the task.
+ /// A Task that can be used to poll or wait for results, or both.
+ Task PutItemAsync(PutItemDocumentOperationRequest request, CancellationToken cancellationToken = default(CancellationToken));
+
+
+
#endregion
#region GetItemAsync
@@ -109,6 +121,15 @@ public partial interface ITable
/// A Task that can be used to poll or wait for results, or both.
Task GetItemAsync(IDictionary key, GetItemOperationConfig config, CancellationToken cancellationToken = default(CancellationToken));
+ ///
+ /// Initiates the asynchronous execution of the GetItem operation.
+ ///
+ /// The GetDocumentOperationRequest object containing all parameters for the GetItem operation.
+ /// Token which can be used to cancel the task.
+ /// A Task that can be used to poll or wait for results, or both.
+ Task GetItemAsync(GetItemDocumentOperationRequest request, CancellationToken cancellationToken = default);
+
+
#endregion
#region UpdateItemAsync
@@ -189,6 +210,15 @@ public partial interface ITable
/// A Task that can be used to poll or wait for results, or both.
Task UpdateItemAsync(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config, CancellationToken cancellationToken = default(CancellationToken));
+ ///
+ /// Initiates the asynchronous execution of the UpdateItem operation using a DocumentUpdateRequest object.
+ ///
+ /// The UpdateItemDocumentOperationRequest object containing all parameters for the update.
+ /// Token which can be used to cancel the task.
+ /// A Task that can be used to poll or wait for results, or both.
+ Task UpdateItemAsync(UpdateItemDocumentOperationRequest request, CancellationToken cancellationToken = default(CancellationToken));
+
+
#endregion
@@ -264,6 +294,14 @@ public partial interface ITable
/// A Task that can be used to poll or wait for results, or both.
Task DeleteItemAsync(IDictionary key, DeleteItemOperationConfig config, CancellationToken cancellationToken = default(CancellationToken));
+ ///
+ /// Initiates the asynchronous execution of the DeleteItem operation.
+ ///
+ /// The DeleteItemDocumentOperationRequest object containing all parameters for the delete operation.
+ /// Token which can be used to cancel the task.
+ /// A Task that can be used to poll or wait for results, or both.
+ Task DeleteItemAsync(DeleteItemDocumentOperationRequest request, CancellationToken cancellationToken = default(CancellationToken));
+
#endregion
}
@@ -292,6 +330,16 @@ public partial class Table : ITable
}
}
+ ///
+ public async Task PutItemAsync(PutItemDocumentOperationRequest request, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(PutItemAsync));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return await PutItemHelperAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
#endregion
#region GetItemAsync
@@ -357,6 +405,16 @@ public partial class Table : ITable
}
}
+ ///
+ public async Task GetItemAsync(GetItemDocumentOperationRequest request, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(GetItemAsync));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return await GetItemHelperAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
#endregion
#region UpdateItemAsync
@@ -441,6 +499,16 @@ public partial class Table : ITable
}
}
+ ///
+ public async Task UpdateItemAsync(UpdateItemDocumentOperationRequest request, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItemAsync));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return await UpdateHelperAsync(request, cancellationToken).ConfigureAwait(false);
+
+ }
+ }
#endregion
@@ -516,15 +584,24 @@ public partial class Table : ITable
}
}
- ///
- public async Task DeleteItemAsync(IDictionary key, DeleteItemOperationConfig config, CancellationToken cancellationToken = default(CancellationToken))
- {
- var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(DeleteItemAsync));
- using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ ///
+ public async Task DeleteItemAsync(IDictionary key, DeleteItemOperationConfig config, CancellationToken cancellationToken = default(CancellationToken))
{
- return await DeleteHelperAsync(MakeKey(key), config, cancellationToken).ConfigureAwait(false);
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(DeleteItemAsync));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return await DeleteHelperAsync(MakeKey(key), config, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ ///
+ public async Task DeleteItemAsync(DeleteItemDocumentOperationRequest request, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(DeleteItemAsync));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return await DeleteHelperAsync(request, cancellationToken).ConfigureAwait(false);
+ }
}
- }
#endregion
diff --git a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs
index 2f170790dc5b..560b9d834dd9 100644
--- a/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs
+++ b/sdk/src/Services/DynamoDBv2/Custom/DocumentModel/_bcl/Table.Sync.cs
@@ -39,6 +39,20 @@ public partial interface ITable
/// True if put is successful or false if the condition in the config was not met.
bool TryPutItem(Document doc, PutItemOperationConfig config = null);
+ ///
+ /// Puts a document into DynamoDB, using optional configs.
+ ///
+ /// The PutItemDocumentOperationRequest object containing all parameters for the PutItem operation.
+ /// Null or updated attributes, depending on request config.
+ Document PutItem(PutItemDocumentOperationRequest request);
+
+ ///
+ /// Puts a document into DynamoDB, using optional configs.
+ ///
+ /// The PutItemDocumentOperationRequest object containing all parameters for the PutItem operation.
+ /// True if put is successful or false if the condition was not met.
+ bool TryPutItem(PutItemDocumentOperationRequest request);
+
#endregion
#region GetItem
@@ -69,6 +83,15 @@ public partial interface ITable
/// Document from DynamoDB.
Document GetItem(IDictionary key, GetItemOperationConfig config = null);
+ ///
+ /// Gets a document from DynamoDB using a request object.
+ ///
+ /// The GetDocumentOperationRequest object containing all parameters for the GetItem operation.
+ /// Document from DynamoDB.
+ Document GetItem(GetItemDocumentOperationRequest request);
+
+
+
#endregion
#region UpdateItem
@@ -159,6 +182,24 @@ public partial interface ITable
///
bool TryUpdateItem(Document doc, Primitive hashKey, Primitive rangeKey, UpdateItemOperationConfig config = null);
+
+ ///
+ /// Initiates the execution of the UpdateItem operation using a DocumentUpdateRequest object.
+ ///
+ /// The UpdateItemDocumentOperationRequest object containing all parameters for the update.
+ /// Null or updated attributes, depending on request.
+ Document UpdateItem(UpdateItemDocumentOperationRequest request);
+
+
+ ///
+ /// Update a document in DynamoDB, with a hash-and-range primary key to identify
+ /// the document, and using the specified config.
+ ///
+ /// The UpdateItemDocumentOperationRequest object containing all parameters for the update.
+ /// True if updated or false if the condition in the config was not met.
+ ///
+ bool TryUpdateItem(UpdateItemDocumentOperationRequest request);
+
#endregion
#region DeleteItem
@@ -237,6 +278,21 @@ public partial interface ITable
/// True if deleted or false if the condition in the config was not met.
bool TryDeleteItem(IDictionary key, DeleteItemOperationConfig config = null);
+ ///
+ /// Delete a document in DynamoDB, identified by a key, using specified configs.
+ ///
+ /// If the condition set on the config fails.
+ /// The DeleteItemDocumentOperationRequest object containing all parameters for the delete operation.
+ /// Null or old attributes, depending on config.
+ Document DeleteItem(DeleteItemDocumentOperationRequest request);
+
+ ///
+ /// Delete a document in DynamoDB, identified by a key, using specified configs.
+ ///
+ /// The DeleteItemDocumentOperationRequest object containing all parameters for the delete operation.
+ /// True if deleted or false if the condition in the config was not met.
+ bool TryDeleteItem(DeleteItemDocumentOperationRequest request);
+
#endregion
}
@@ -272,6 +328,35 @@ public bool TryPutItem(Document doc, PutItemOperationConfig config = null)
}
}
+ ///
+ public Document PutItem(PutItemDocumentOperationRequest request)
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(PutItem));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return PutItemHelper(request);
+ }
+ }
+
+ ///
+ public bool TryPutItem(PutItemDocumentOperationRequest request)
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(TryPutItem));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ try
+ {
+ PutItemHelper(request);
+ return true;
+ }
+ catch (ConditionalCheckFailedException)
+ {
+ return false;
+ }
+ }
+ }
+
+
#endregion
@@ -308,6 +393,17 @@ public Document GetItem(IDictionary key, GetItemOperation
}
+ ///
+ public Document GetItem(GetItemDocumentOperationRequest request)
+ {
+
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(GetItem));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return GetItemHelper(request);
+ }
+ }
+
#endregion
@@ -425,6 +521,34 @@ public bool TryUpdateItem(Document doc, Primitive hashKey, Primitive rangeKey, U
}
}
+ ///
+ public Document UpdateItem(UpdateItemDocumentOperationRequest request)
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(UpdateItem));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return UpdateHelper(request);
+ }
+ }
+
+ ///
+ public bool TryUpdateItem(UpdateItemDocumentOperationRequest request)
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(TryUpdateItem));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ try
+ {
+ UpdateHelper(request);
+ return true;
+ }
+ catch (ConditionalCheckFailedException)
+ {
+ return false;
+ }
+ }
+ }
+
#endregion
@@ -542,6 +666,34 @@ public bool TryDeleteItem(IDictionary key, DeleteItemOper
}
}
+ ///
+ public Document DeleteItem(DeleteItemDocumentOperationRequest request)
+ {
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(DeleteItem));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ return DeleteHelper(request);
+ }
+ }
+
+ ///
+ public bool TryDeleteItem(DeleteItemDocumentOperationRequest request)
+ {
+
+ var operationName = DynamoDBTelemetry.ExtractOperationName(nameof(Table), nameof(TryDeleteItem));
+ using (DynamoDBTelemetry.CreateSpan(TracerProvider, operationName, spanKind: SpanKind.CLIENT))
+ {
+ try
+ {
+ DeleteHelper(request);
+ return true;
+ }
+ catch (ConditionalCheckFailedException)
+ {
+ return false;
+ }
+ }
+ }
#endregion
}
diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
index 433b7c8b4ea0..8d4f6d853dac 100644
--- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
+++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DataModelTests.cs
@@ -1122,6 +1122,57 @@ public void TestContext_Scan_WithExpressionFilter()
Assert.AreEqual("Sam", upcomingEnumResult[0].Name);
}
+ [TestMethod]
+ [TestCategory("DynamoDBv2")]
+ public void TestContext_SaveItem_WithTTL()
+ {
+ TableCache.Clear();
+ CleanupTables();
+ TableCache.Clear();
+
+ // Create context and table if needed
+ CreateContext(DynamoDBEntryConversion.V2, true);
+
+ // Define TTL to be 2 minutes in the future
+ var ttlEpoch = DateTimeOffset.UtcNow.AddMinutes(2).ToUnixTimeSeconds();
+
+ var item = new TtlTestItem
+ {
+ Id = 1,
+ Data = "Test with TTL",
+ Ttl = ttlEpoch
+ };
+
+ Context.Save(item);
+
+ // Load immediately, should exist
+ var loaded = Context.Load(item.Id);
+ Assert.IsNotNull(loaded);
+ Assert.AreEqual(item.Id, loaded.Id);
+ Assert.AreEqual(item.Data, loaded.Data);
+ Assert.AreEqual(item.Ttl, loaded.Ttl);
+
+ item.Ttl = DateTimeOffset.UtcNow.AddMinutes(3).ToUnixTimeSeconds();
+ Context.Save(item);
+ var loaded2 = Context.Load(item.Id);
+ Assert.IsNotNull(loaded2);
+ Assert.AreEqual(item.Id, loaded2.Id);
+ Assert.AreEqual(item.Data, loaded2.Data);
+ Assert.AreEqual(item.Ttl, loaded2.Ttl);
+ }
+
+ // Example model for TTL
+ [DynamoDBTable("HashTable")]
+ public class TtlTestItem
+ {
+ [DynamoDBHashKey]
+ public int Id { get; set; }
+
+ public string Data { get; set; }
+
+ [DynamoDBProperty("TTL")]
+ public long Ttl { get; set; }
+ }
[TestMethod]
[TestCategory("DynamoDBv2")]
diff --git a/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs b/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs
index f3d230d8fac4..c99b9cf0dc47 100644
--- a/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs
+++ b/sdk/test/Services/DynamoDBv2/IntegrationTests/DocumentTests.cs
@@ -87,6 +87,11 @@ public void TestTableOperations()
// Test Count on Query
TestSelectCountOnQuery(hashTable);
+
+ TestExpressionPutWithDocumentOperationRequest(hashTable);
+ TestExpressionUpdateWithDocumentOperationRequest(hashTable);
+ TestExpressionsOnDeleteWithDocumentOperationRequest(hashTable);
+
}
}
@@ -167,6 +172,11 @@ public void TestTableOperationsViaBuilder()
// Test that attributes stored as Datetimes can be retrieved in UTC.
TestAsDateTimeUtc(numericHashRangeTable);
+
+ TestExpressionPutWithDocumentOperationRequest(hashTable);
+ TestExpressionUpdateWithDocumentOperationRequest(hashTable);
+ TestExpressionsOnDeleteWithDocumentOperationRequest(hashTable);
+
}
}
@@ -2324,6 +2334,148 @@ private void TestSelectCountOnQuery(ITable hashTable)
Assert.AreEqual(0, docs.Count);
}
+ private void TestExpressionPutWithDocumentOperationRequest(ITable table)
+ {
+ var doc = new Document
+ {
+ ["Id"] = DateTime.UtcNow.Ticks,
+ ["name"] = "docop-conditional-form"
+ };
+
+ table.PutItem(doc);
+
+ var conditionalExpression = new Expression
+ {
+ ExpressionStatement = "attribute_not_exists(referencecounter) OR referencecounter = :zero",
+ ExpressionAttributeValues = { [":zero"] = 0 }
+ };
+
+ var putRequest = new PutItemDocumentOperationRequest
+ {
+ Document = doc,
+ ConditionalExpression = conditionalExpression
+ };
+
+ doc["update-test"] = 1;
+ Assert.IsTrue(table.TryPutItem(putRequest));
+
+ doc["referencecounter"] = 0;
+ table.UpdateItem(doc);
+
+ doc["update-test"] = null;
+ Assert.IsTrue(table.TryPutItem(new PutItemDocumentOperationRequest { Document = doc, ConditionalExpression = conditionalExpression }));
+
+ var reloaded = table.GetItem(doc);
+ Assert.IsFalse(reloaded.Contains("update-test"));
+
+ doc["referencecounter"] = 1;
+ table.UpdateItem(doc);
+
+ doc["update-test"] = 3;
+ Assert.IsFalse(table.TryPutItem(new PutItemDocumentOperationRequest { Document = doc, ConditionalExpression = conditionalExpression }));
+
+ table.DeleteItem(doc);
+ }
+
+ private void TestExpressionUpdateWithDocumentOperationRequest(ITable table)
+ {
+ var doc = new Document
+ {
+ ["Id"] = DateTime.UtcNow.Ticks,
+ ["name"] = "docop-update-conditional"
+ };
+ table.PutItem(doc);
+
+ var conditionalExpression = new Expression
+ {
+ ExpressionStatement = "attribute_not_exists(referencecounter) OR referencecounter = :zero",
+ ExpressionAttributeValues = { [":zero"] = 0 }
+ };
+
+ var config = new UpdateItemOperationConfig
+ {
+ ConditionalExpression = conditionalExpression
+ };
+
+ doc["update-test"] = 1;
+ Assert.IsTrue(table.TryUpdateItem(new UpdateItemDocumentOperationRequest
+ {
+ Document = doc,
+ ConditionalExpression = conditionalExpression
+ }));
+
+ doc["referencecounter"] = 0;
+ table.UpdateItem(doc);
+
+ doc["update-test"] = null;
+ Assert.IsTrue(table.TryUpdateItem(new UpdateItemDocumentOperationRequest
+ {
+ Document = doc,
+ ConditionalExpression = conditionalExpression
+ }));
+
+ var reloaded = table.GetItem(doc);
+ Assert.IsFalse(reloaded.Contains("update-test"));
+
+ doc["referencecounter"] = 1;
+ table.UpdateItem(doc);
+
+ doc["update-test"] = 3;
+ Assert.IsFalse(table.TryUpdateItem(new UpdateItemDocumentOperationRequest
+ {
+ Document = doc,
+ ConditionalExpression = conditionalExpression
+ }));
+
+ table.DeleteItem(doc);
+ }
+
+ private void TestExpressionsOnDeleteWithDocumentOperationRequest(ITable table)
+ {
+ var doc = new Document
+ {
+ ["Id"] = 9001,
+ ["Price"] = 6
+ };
+ table.PutItem(doc);
+
+ var key = new Dictionary
+ {
+ { "Id", doc["Id"] }
+ };
+
+ var expression = new Expression
+ {
+ ExpressionStatement = "Price > :price",
+ ExpressionAttributeValues = { [":price"] = 7 }
+ };
+
+ var failingRequest = new DeleteItemDocumentOperationRequest
+ {
+ Key = key,
+ ConditionalExpression = expression,
+ ReturnValues = ReturnValues.AllOldAttributes
+ };
+
+ Assert.IsFalse(table.TryDeleteItem(failingRequest));
+ Assert.IsNotNull(table.GetItem(doc));
+
+ expression.ExpressionAttributeValues[":price"] = 4;
+
+ var succeedingRequest = new DeleteItemDocumentOperationRequest
+ {
+ Key = key,
+ ConditionalExpression = expression,
+ ReturnValues = ReturnValues.AllOldAttributes
+ };
+
+ var oldAttributes = table.DeleteItem(succeedingRequest);
+ Assert.IsNotNull(oldAttributes);
+ Assert.AreEqual(6, oldAttributes["Price"].AsInt());
+
+ Assert.IsNull(table.GetItem(doc));
+ }
+
private bool AreValuesEqual(Document docA, Document docB, DynamoDBEntryConversion conversion = null)
{
if (conversion != null)
diff --git a/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/TableTests.cs b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/TableTests.cs
new file mode 100644
index 000000000000..972149439ee1
--- /dev/null
+++ b/sdk/test/Services/DynamoDBv2/UnitTests/Custom/DocumentModel/TableTests.cs
@@ -0,0 +1,936 @@
+using Amazon;
+using Amazon.DynamoDBv2;
+using Amazon.DynamoDBv2.DocumentModel;
+using Amazon.DynamoDBv2.Model;
+using Amazon.Runtime;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AWSSDK_DotNet.UnitTests
+{
+ [TestClass]
+ public class TableTests
+ {
+ private Mock _ddbClientMock;
+ private Table _table;
+ private string _tableName = "TestTable";
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _ddbClientMock = new Mock(MockBehavior.Strict);
+
+ var clientConfigMock = new Mock();
+ // Setup any properties/methods you expect to be used, e.g.:
+ clientConfigMock.SetupGet(c => c.RegionEndpoint).Returns((RegionEndpoint)null);
+ clientConfigMock.SetupGet(c => c.ServiceURL).Returns((string)null);
+ // Add more setups as needed for your tests
+
+ // Setup the Config property on the IAmazonDynamoDB mock
+ _ddbClientMock.SetupGet(c => c.Config).Returns(clientConfigMock.Object);
+
+ var config = new TableConfig(_tableName);
+
+ _table = new Table(_ddbClientMock.Object, config);
+ _table.ClearTableData();
+ _table.Keys.Add("Id", new KeyDescription { IsHash = true, Type = DynamoDBEntryType.String });
+ _table.HashKeys.Add("Id");
+ }
+
+ #region Helpers
+
+ private Task InvokeUpdateAsync(UpdateItemDocumentOperationRequest request)
+ => _table.UpdateHelperAsync(request, CancellationToken.None);
+
+ private Document InvokeUpdateSync(UpdateItemDocumentOperationRequest request)
+ => _table.UpdateHelper(request);
+ private Task InvokeDeleteAsync(DeleteItemDocumentOperationRequest request)
+ => _table.DeleteHelperAsync(request, CancellationToken.None);
+
+ private Document InvokeDeleteSync(DeleteItemDocumentOperationRequest request)
+ => _table.DeleteHelper(request);
+
+ private Task InvokePutAsync(PutItemDocumentOperationRequest request)
+ => _table.PutItemAsync(request, CancellationToken.None);
+
+ private Document InvokePutSync(PutItemDocumentOperationRequest request)
+ => _table.PutItem(request);
+
+ private Task InvokeGetAsync(GetItemDocumentOperationRequest request)
+ => _table.GetItemAsync(request, CancellationToken.None);
+
+ private Document InvokeGetSync(GetItemDocumentOperationRequest request)
+ => _table.GetItem(request);
+
+ private async Task AssertThrowsAsync(Func act, string expectedMessage = null) where T : Exception
+ {
+ var ex = await Assert.ThrowsExceptionAsync(act);
+ if (expectedMessage != null)
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+
+ private void AssertThrowsSync(Action act, string expectedMessage = null) where T : Exception
+ {
+ var ex = Assert.ThrowsException(act);
+ if (expectedMessage != null)
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+
+ #endregion
+
+ #region UpdateHelper Tests
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_RequestNull_ThrowsArgumentNullException(bool isAsync)
+ {
+ if (isAsync)
+ await AssertThrowsAsync(() => InvokeUpdateAsync(null));
+ else
+ AssertThrowsSync(() => InvokeUpdateSync(null));
+ }
+
+ [DataTestMethod]
+ [DataRow(null, null, "Either Document or UpdateExpression must be set (exclusively).", true)]
+ [DataRow("doc", "expr", "Either Document or UpdateExpression must be set (exclusively).", true)]
+ [DataRow(null, null, "Either Document or UpdateExpression must be set (exclusively).", false)]
+ [DataRow("doc", "expr", "Either Document or UpdateExpression must be set (exclusively).", false)]
+ public async Task UpdateHelper_RequestInvalidDocExprCombination_ThrowsInvalidOperationException(object docObj, object exprObj, string expectedMessage, bool isAsync)
+ {
+ Document doc = docObj as Document;
+ Expression expr = exprObj as Expression;
+
+ if (docObj is string && (string)docObj == "doc")
+ doc = new Document { ["Id"] = "1", ["Count"] = 5 };
+ if (exprObj is string && (string)exprObj == "expr")
+ expr = new Expression { ExpressionStatement = "test" };
+
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ Document = doc,
+ UpdateExpression = expr
+ };
+
+ if (isAsync)
+ await AssertThrowsAsync(() => InvokeUpdateAsync(request), expectedMessage);
+ else
+ AssertThrowsSync(() => InvokeUpdateSync(request), expectedMessage);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_KeyMissing_ThrowsInvalidOperationException(bool isAsync)
+ {
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ UpdateExpression = new Expression { ExpressionStatement = "test" },
+ Key = null
+ };
+ _table.HashKeys.Clear();
+ _table.Keys.Clear();
+
+ if (isAsync)
+ await AssertThrowsAsync(() => InvokeUpdateAsync(request));
+ else
+ AssertThrowsSync(() => InvokeUpdateSync(request));
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_ReturnsDocument_WhenReturnValuesNotNone(bool isAsync)
+ {
+ var docMock = new Document { ["Id"] = "1", ["Count"] = 5 };
+
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ Document = docMock,
+ UpdateExpression = null,
+ Key = new Dictionary { { "Id", new Primitive("abc") } },
+ ReturnValues = ReturnValues.AllOldAttributes
+ };
+
+ var updateItemResponse = new UpdateItemResponse
+ {
+ Attributes = new Dictionary { { "Id", new AttributeValue { S = "abc" } } }
+ };
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.UpdateItemAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(updateItemResponse);
+ else
+ _ddbClientMock.Setup(c => c.UpdateItem(It.IsAny()))
+ .Returns(updateItemResponse);
+
+ var result = isAsync
+ ? await InvokeUpdateAsync(request)
+ : InvokeUpdateSync(request);
+
+ Assert.IsNotNull(result);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_SetsUpdateExpression_WhenDocumentProvided(bool isAsync)
+ {
+ var doc = new Document { ["Id"] = "1", ["Count"] = 5 };
+
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ Document = doc,
+ UpdateExpression = null,
+ Key = null,
+ ReturnValues = ReturnValues.AllNewAttributes
+ };
+
+ var updateItemResponse = new UpdateItemResponse
+ {
+ Attributes = new Dictionary { { "Id", new AttributeValue { S = "1" } }, { "Count", new AttributeValue { N = "5" } } }
+ };
+
+ if (isAsync)
+ {
+ _ddbClientMock.Setup(c => c.UpdateItemAsync(
+ It.Is(r => r.Key.ContainsKey("Id")
+ && r.UpdateExpression != null
+ && r.ExpressionAttributeValues != null
+ && r.ExpressionAttributeNames != null), It.IsAny()))
+ .ReturnsAsync(updateItemResponse)
+ .Verifiable();
+ }
+ else
+ {
+ _ddbClientMock.Setup(c => c.UpdateItem(
+ It.Is(r => r.Key.ContainsKey("Id")
+ && r.UpdateExpression != null
+ && r.ExpressionAttributeValues != null
+ && r.ExpressionAttributeNames != null)))
+ .Returns(updateItemResponse)
+ .Verifiable();
+ }
+
+ var result = isAsync ? await InvokeUpdateAsync(request) : InvokeUpdateSync(request);
+
+ Assert.IsNotNull(result);
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.UpdateItemAsync(
+ It.Is(r => r.UpdateExpression != null
+ && r.ExpressionAttributeValues != null
+ && r.ExpressionAttributeNames != null),
+ It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.UpdateItem(
+ It.Is(r => r.UpdateExpression != null
+ && r.ExpressionAttributeValues != null
+ && r.ExpressionAttributeNames != null)), Times.Once);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_UsesOnlyChangedAttributes_WhenKeysUnchanged(bool isAsync)
+ {
+ var doc = new Document { ["Id"] = "1", ["Count"] = 5, ["Prop"] = "test" };
+ doc.CommitChanges();
+ doc["Count"] = 10;
+ doc["Prop2"] = "test";
+
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ Document = doc,
+ UpdateExpression = null,
+ Key = null,
+ ReturnValues = ReturnValues.AllNewAttributes
+ };
+
+ var updateItemResponse = new UpdateItemResponse
+ {
+ Attributes = new Dictionary
+ { { "Id", new AttributeValue { S = "1" } }, { "Count", new AttributeValue { N = "5" } } }
+ };
+
+ Predicate predicate = r =>
+ r.Key.ContainsKey("Id")
+ && r.UpdateExpression != null
+ && r.ExpressionAttributeValues.Count == 2
+ && r.ExpressionAttributeNames.ContainsValue("Count")
+ && r.ExpressionAttributeNames.ContainsValue("Prop2")
+ && !r.ExpressionAttributeNames.ContainsValue("Prop");
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(updateItemResponse)
+ .Verifiable();
+ else
+ _ddbClientMock.Setup(c => c.UpdateItem(It.Is(r => predicate(r))))
+ .Returns(updateItemResponse)
+ .Verifiable();
+
+ var result = isAsync ? await InvokeUpdateAsync(request) : InvokeUpdateSync(request);
+
+ Assert.IsNotNull(result);
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.UpdateItem(It.Is(r => predicate(r))), Times.Once);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_AppliesUpdateExpression(bool isAsync)
+ {
+ var updateExpr = new Expression()
+ {
+ ExpressionStatement = "SET #N = :v",
+ ExpressionAttributeNames = new Dictionary { { "#N", "test" } },
+ ExpressionAttributeValues = new Dictionary { { ":v", "test" } },
+ };
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ Document = null,
+ UpdateExpression = updateExpr,
+ ConditionalExpression = null,
+ Key = new Dictionary { { "Id", new Primitive("abc") } }
+ };
+
+ Predicate predicate = r =>
+ r.Key.ContainsKey("Id")
+ && r.UpdateExpression == "SET #N = :v"
+ && r.ExpressionAttributeValues.Count == 1;
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(new UpdateItemResponse());
+ else
+ _ddbClientMock.Setup(c => c.UpdateItem(It.Is(r => predicate(r))))
+ .Returns(new UpdateItemResponse());
+
+ if (isAsync)
+ await InvokeUpdateAsync(request);
+ else
+ InvokeUpdateSync(request);
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.UpdateItem(It.Is(r => predicate(r))), Times.Once);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_AppliesConditionalExpression(bool isAsync)
+ {
+ var condExpr = new Expression()
+ {
+ ExpressionStatement = "#N = :v",
+ ExpressionAttributeNames = new Dictionary { { "#N", "test" } },
+ ExpressionAttributeValues = new Dictionary { { ":v", "test" } },
+ };
+ var doc = new Document { ["Id"] = "1", ["Count"] = 5, ["Prop"] = "test" };
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ Document = doc,
+ UpdateExpression = null,
+ ConditionalExpression = condExpr,
+ Key = new Dictionary { { "Id", new Primitive("abc") } }
+ };
+
+ Predicate predicate = r =>
+ r.Key.ContainsKey("Id")
+ && r.ConditionExpression == "#N = :v"
+ && r.ExpressionAttributeValues.Count == 3; // :v + converted Count + Prop
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(new UpdateItemResponse());
+ else
+ _ddbClientMock.Setup(c => c.UpdateItem(It.Is(r => predicate(r))))
+ .Returns(new UpdateItemResponse());
+
+ if (isAsync)
+ await InvokeUpdateAsync(request);
+ else
+ InvokeUpdateSync(request);
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.UpdateItem(It.Is(r => predicate(r))), Times.Once);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task UpdateHelper_AppliesBothExpressions(bool isAsync)
+ {
+ var updateExpr = new Expression()
+ {
+ ExpressionStatement = "SET #Nu = :vu",
+ ExpressionAttributeNames = new Dictionary { { "#Nu", "test" } },
+ ExpressionAttributeValues = new Dictionary { { ":vu", "test" } },
+ };
+ var condExpr = new Expression()
+ {
+ ExpressionStatement = "#Nc = :vc",
+ ExpressionAttributeNames = new Dictionary { { "#Nc", "test" } },
+ ExpressionAttributeValues = new Dictionary { { ":vc", "test" } },
+ };
+ var request = new UpdateItemDocumentOperationRequest
+ {
+ Document = null,
+ UpdateExpression = updateExpr,
+ ConditionalExpression = condExpr,
+ Key = new Dictionary { { "Id", new Primitive("abc") } }
+ };
+
+ Predicate predicate = r =>
+ r.Key.ContainsKey("Id")
+ && r.ConditionExpression == "#Nc = :vc"
+ && r.UpdateExpression == "SET #Nu = :vu"
+ && r.ExpressionAttributeValues.Count == 2;
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(new UpdateItemResponse());
+ else
+ _ddbClientMock.Setup(c => c.UpdateItem(It.Is(r => predicate(r))))
+ .Returns(new UpdateItemResponse());
+
+ if (isAsync)
+ await InvokeUpdateAsync(request);
+ else
+ InvokeUpdateSync(request);
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.UpdateItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.UpdateItem(It.Is(r => predicate(r))), Times.Once);
+ }
+
+ #endregion
+
+ #region DeleteHelper Tests
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task DeleteHelper_RequestNull_ThrowsArgumentNullException(bool isAsync)
+ {
+ if (isAsync)
+ await AssertThrowsAsync(() => InvokeDeleteAsync(null));
+ else
+ AssertThrowsSync(() => InvokeDeleteSync(null));
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task DeleteHelper_ReturnsDocument_WhenReturnValuesAllOldAttributes(bool isAsync)
+ {
+ var request = new DeleteItemDocumentOperationRequest
+ {
+ Key = new Dictionary { { "Id", new Primitive("abc") } },
+ ReturnValues = ReturnValues.AllOldAttributes
+ };
+
+ var deleteResponse = new DeleteItemResponse
+ {
+ Attributes = new Dictionary { { "Id", new AttributeValue { S = "abc" } } }
+ };
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.DeleteItemAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(deleteResponse);
+ else
+ _ddbClientMock.Setup(c => c.DeleteItem(It.IsAny()))
+ .Returns(deleteResponse);
+
+ var result = isAsync ? await InvokeDeleteAsync(request) : InvokeDeleteSync(request);
+
+ Assert.IsNotNull(result);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task DeleteHelper_ReturnsNull_WhenReturnValuesNone(bool isAsync)
+ {
+ var request = new DeleteItemDocumentOperationRequest
+ {
+ Key = new Dictionary { { "Id", new Primitive("xyz") } },
+ ReturnValues = ReturnValues.None
+ };
+
+ var deleteResponse = new DeleteItemResponse
+ {
+ Attributes = new Dictionary { { "Id", new AttributeValue { S = "xyz" } } }
+ };
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.DeleteItemAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(deleteResponse);
+ else
+ _ddbClientMock.Setup(c => c.DeleteItem(It.IsAny()))
+ .Returns(deleteResponse);
+
+ var result = isAsync ? await InvokeDeleteAsync(request) : InvokeDeleteSync(request);
+
+ Assert.IsNull(result);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task DeleteHelper_AppliesConditionalExpression(bool isAsync)
+ {
+ var condExpr = new Expression
+ {
+ ExpressionStatement = "#A = :v",
+ ExpressionAttributeNames = new Dictionary { { "#A", "Attr" } },
+ ExpressionAttributeValues = new Dictionary { { ":v", "val" } }
+ };
+
+ var request = new DeleteItemDocumentOperationRequest
+ {
+ Key = new Dictionary { { "Id", new Primitive("abc") } },
+ ConditionalExpression = condExpr,
+ ReturnValues = ReturnValues.None
+ };
+
+ Predicate predicate = r =>
+ r.Key.ContainsKey("Id")
+ && r.ConditionExpression == "#A = :v"
+ && r.ExpressionAttributeValues.Count == 1;
+
+ var deleteResponse = new DeleteItemResponse();
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.DeleteItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(deleteResponse)
+ .Verifiable();
+ else
+ _ddbClientMock.Setup(c => c.DeleteItem(It.Is(r => predicate(r))))
+ .Returns(deleteResponse)
+ .Verifiable();
+
+ var result = isAsync ? await InvokeDeleteAsync(request) : InvokeDeleteSync(request);
+
+ Assert.IsNull(result); // ReturnValues.None
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.DeleteItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.DeleteItem(It.Is(r => predicate(r))), Times.Once);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task DeleteHelper_AppliesConditionalExpression_WithAllOldAttributes(bool isAsync)
+ {
+ var condExpr = new Expression
+ {
+ ExpressionStatement = "#A = :v",
+ ExpressionAttributeNames = new Dictionary { { "#A", "Attr" } },
+ ExpressionAttributeValues = new Dictionary { { ":v", "val" } }
+ };
+
+ var request = new DeleteItemDocumentOperationRequest
+ {
+ Key = new Dictionary { { "Id", new Primitive("abc") } },
+ ConditionalExpression = condExpr,
+ ReturnValues = ReturnValues.AllOldAttributes
+ };
+
+ var deleteResponse = new DeleteItemResponse
+ {
+ Attributes = new Dictionary
+ {
+ { "Id", new AttributeValue { S = "abc" } },
+ { "Attr", new AttributeValue { S = "val" } }
+ }
+ };
+
+ Predicate predicate = r =>
+ r.Key.ContainsKey("Id")
+ && r.ConditionExpression == "#A = :v"
+ && r.ExpressionAttributeValues.Count == 1;
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.DeleteItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(deleteResponse)
+ .Verifiable();
+ else
+ _ddbClientMock.Setup(c => c.DeleteItem(It.Is(r => predicate(r))))
+ .Returns(deleteResponse)
+ .Verifiable();
+
+ var result = isAsync ? await InvokeDeleteAsync(request) : InvokeDeleteSync(request);
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual("abc", result["Id"].AsPrimitive().Value);
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.DeleteItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.DeleteItem(It.Is(r => predicate(r))), Times.Once);
+ }
+
+ [DataTestMethod]
+ [DataRow(true, true)]
+ [DataRow(true, false)]
+ [DataRow(false, true)]
+ [DataRow(false, false)]
+ public async Task DeleteHelper_KeyNullOrEmpty_ThrowsInvalidOperationException(bool isAsync, bool keyIsNull)
+ {
+ var request = new DeleteItemDocumentOperationRequest
+ {
+ Key = keyIsNull ? null : new Dictionary(),
+ ReturnValues = ReturnValues.None
+ };
+
+ const string expectedMessage = "DeleteItemDocumentOperationRequest.Key cannot be null or empty.";
+
+ if (isAsync)
+ {
+ var ex = await Assert.ThrowsExceptionAsync(() => InvokeDeleteAsync(request));
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+ else
+ {
+ var ex = Assert.ThrowsException(() => InvokeDeleteSync(request));
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+ }
+
+ #endregion
+
+ #region PutItemHelper Tests
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task PutItemHelper_RequestNull_ThrowsArgumentNullException(bool isAsync)
+ {
+ if (isAsync)
+ await AssertThrowsAsync(() => InvokePutAsync(null));
+ else
+ AssertThrowsSync(() => InvokePutSync(null));
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task PutItemHelper_ReturnsNull_WhenReturnValuesNone(bool isAsync)
+ {
+ var doc = new Document { ["Id"] = "1", ["Value"] = "A" };
+ var request = new PutItemDocumentOperationRequest
+ {
+ Document = doc,
+ ReturnValues = ReturnValues.None
+ };
+
+ var putResponse = new PutItemResponse
+ {
+ Attributes = new Dictionary { { "Id", new AttributeValue { S = "1" } }, { "Value", new AttributeValue { S = "A" } } }
+ };
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.PutItemAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(putResponse);
+ else
+ _ddbClientMock.Setup(c => c.PutItem(It.IsAny()))
+ .Returns(putResponse);
+
+ var result = isAsync ? await InvokePutAsync(request) : InvokePutSync(request);
+
+ Assert.IsNull(result);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task PutItemHelper_ReturnsOldAttributes_WhenReturnValuesAllOld(bool isAsync)
+ {
+ var doc = new Document { ["Id"] = "1", ["Value"] = "A" };
+ var request = new PutItemDocumentOperationRequest
+ {
+ Document = doc,
+ ReturnValues = ReturnValues.AllOldAttributes
+ };
+
+ var oldAttributes = new Dictionary
+ {
+ { "Id", new AttributeValue { S = "1" } },
+ { "Value", new AttributeValue { S = "OLD" } }
+ };
+
+ var putResponse = new PutItemResponse { Attributes = oldAttributes };
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.PutItemAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(putResponse);
+ else
+ _ddbClientMock.Setup(c => c.PutItem(It.IsAny()))
+ .Returns(putResponse);
+
+ var result = isAsync ? await InvokePutAsync(request) : InvokePutSync(request);
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual("OLD", result["Value"].AsPrimitive().Value);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task PutItemHelper_CommitsDocumentChanges(bool isAsync)
+ {
+ var doc = new Document { ["Id"] = "1", ["Value"] = "A" };
+ // Initially document has changes
+ Assert.IsTrue(doc.IsDirty());
+
+ var request = new PutItemDocumentOperationRequest
+ {
+ Document = doc,
+ ReturnValues = ReturnValues.None
+ };
+
+ var putResponse = new PutItemResponse { Attributes = new Dictionary() };
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.PutItemAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(putResponse);
+ else
+ _ddbClientMock.Setup(c => c.PutItem(It.IsAny()))
+ .Returns(putResponse);
+
+ if (isAsync)
+ await InvokePutAsync(request);
+ else
+ InvokePutSync(request);
+
+ Assert.IsFalse(doc.IsDirty(), "Document should have committed changes after PutItem.");
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task PutItemHelper_PassesConditionalExpression(bool isAsync)
+ {
+ var doc = new Document { ["Id"] = "1", ["Value"] = "A" };
+ var condExpr = new Expression
+ {
+ ExpressionStatement = "attribute_not_exists(#PK)",
+ ExpressionAttributeNames = new Dictionary { { "#PK", "Id" } }
+ };
+
+ var request = new PutItemDocumentOperationRequest
+ {
+ Document = doc,
+ ConditionalExpression = condExpr,
+ ReturnValues = ReturnValues.None
+ };
+
+ Predicate predicate = r =>
+ r.Item.ContainsKey("Id") &&
+ r.ConditionExpression == "attribute_not_exists(#PK)" &&
+ r.ExpressionAttributeNames.ContainsKey("#PK");
+
+ var putResponse = new PutItemResponse();
+
+ if (isAsync)
+ _ddbClientMock.Setup(c => c.PutItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(putResponse);
+ else
+ _ddbClientMock.Setup(c => c.PutItem(It.Is(r => predicate(r))))
+ .Returns(putResponse);
+
+ if (isAsync)
+ await InvokePutAsync(request);
+ else
+ InvokePutSync(request);
+
+ if (isAsync)
+ _ddbClientMock.Verify(c => c.PutItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ else
+ _ddbClientMock.Verify(c => c.PutItem(It.Is(r => predicate(r))), Times.Once);
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task PutItemHelper_DocumentNull_ThrowsInvalidOperationException(bool isAsync)
+ {
+ var request = new PutItemDocumentOperationRequest
+ {
+ Document = null,
+ ReturnValues = ReturnValues.None
+ };
+
+ const string expectedMessage = "The Document property of the PutItemDocumentOperationRequest cannot be null.";
+
+ if (isAsync)
+ {
+ var ex = await Assert.ThrowsExceptionAsync(() => InvokePutAsync(request));
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+ else
+ {
+ var ex = Assert.ThrowsException(() => InvokePutSync(request));
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+ }
+ #endregion
+
+ #region GetItemHelper Tests
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task GetItemHelper_RequestNull_ThrowsArgumentNullException(bool isAsync)
+ {
+ if (isAsync)
+ {
+ await AssertThrowsAsync(() => InvokeGetAsync(null));
+ }
+ else
+ {
+ AssertThrowsSync(() => InvokeGetSync(null));
+ }
+ }
+
+ [DataTestMethod]
+ [DataRow(true, true)]
+ [DataRow(true, false)]
+ [DataRow(false, true)]
+ [DataRow(false, false)]
+ public async Task GetItemHelper_KeyNullOrEmpty_ThrowsInvalidOperationException(bool isAsync, bool keyIsNull)
+ {
+ var request = new GetItemDocumentOperationRequest
+ {
+ Key = keyIsNull ? null : new Dictionary(),
+ ConsistentRead = false
+ };
+
+ const string expectedMessage = "GetItemDocumentOperationRequest.Key cannot be null or empty.";
+
+ if (isAsync)
+ {
+ var ex = await Assert.ThrowsExceptionAsync(() => InvokeGetAsync(request));
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+ else
+ {
+ var ex = Assert.ThrowsException(() => InvokeGetSync(request));
+ Assert.AreEqual(expectedMessage, ex.Message);
+ }
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task GetItemHelper_AppliesProjectionExpression(bool isAsync)
+ {
+ var request = new GetItemDocumentOperationRequest
+ {
+ Key = new Dictionary { { "Id", new Primitive("abc") } },
+ ConsistentRead = true,
+ ProjectionExpression = new Expression
+ {
+ ExpressionStatement = "#A, #B",
+ ExpressionAttributeNames = new Dictionary
+ {
+ { "#A", "AttrA" },
+ { "#B", "AttrB" }
+ }
+ }
+ };
+
+ Predicate predicate = r =>
+ r.Key.ContainsKey("Id")
+ && r.ConsistentRead == true
+ && r.ProjectionExpression == "#A, #B"
+ && r.ExpressionAttributeNames.ContainsKey("#A")
+ && r.ExpressionAttributeNames.ContainsKey("#B");
+
+ var getResponse = new GetItemResponse { Item = new Dictionary { { "AttrA", new AttributeValue { S = "valA" } } } };
+
+ if (isAsync)
+ {
+ _ddbClientMock
+ .Setup(c => c.GetItemAsync(It.Is(r => predicate(r)), It.IsAny()))
+ .ReturnsAsync(getResponse)
+ .Verifiable();
+
+ var result = await InvokeGetAsync(request);
+ Assert.IsNotNull(result);
+ _ddbClientMock.Verify(c => c.GetItemAsync(It.Is(r => predicate(r)), It.IsAny()), Times.Once);
+ }
+ else
+ {
+ _ddbClientMock
+ .Setup(c => c.GetItem(It.Is(r => predicate(r))))
+ .Returns(getResponse)
+ .Verifiable();
+
+ var result = InvokeGetSync(request);
+ Assert.IsNotNull(result);
+ _ddbClientMock.Verify(c => c.GetItem(It.Is(r => predicate(r))), Times.Once);
+ }
+ }
+
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task GetItemHelper_ReturnsDocument(bool isAsync)
+ {
+ var request = new GetItemDocumentOperationRequest
+ {
+ Key = new Dictionary { { "Id", new Primitive("123") } },
+ ConsistentRead = false
+ };
+
+ var getResponse = new GetItemResponse
+ {
+ Item = new Dictionary
+ {
+ { "Id", new AttributeValue { S = "123" } },
+ { "Name", new AttributeValue { S = "John" } }
+ }
+ };
+
+ if (isAsync)
+ {
+ _ddbClientMock.Setup(c => c.GetItemAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(getResponse);
+
+ var result = await InvokeGetAsync(request);
+ Assert.IsNotNull(result);
+ Assert.AreEqual("123", result["Id"].AsPrimitive().Value);
+ Assert.AreEqual("John", result["Name"].AsPrimitive().Value);
+ }
+ else
+ {
+ _ddbClientMock.Setup(c => c.GetItem(It.IsAny()))
+ .Returns(getResponse);
+
+ var result = InvokeGetSync(request);
+ Assert.IsNotNull(result);
+ Assert.AreEqual("123", result["Id"].AsPrimitive().Value);
+ Assert.AreEqual("John", result["Name"].AsPrimitive().Value);
+ }
+ }
+ #endregion
+ }
+}