Closed
Description
Describe the bug
When using TransactWrite<T>.AddSaveItem
and T
only contains a DynamoDBHashKey
/ DynamoDBRangeKey
executing the transaction results in an exception.
Expected Behavior
Expect that the update / write succeeds
Current Behavior
Exception
{"Timestamp":"2023-11-07T12:50:21.7307220-08:00","Level":"Error","MessageTemplate":"An unhandled exception has occurred while executing the request.","Exception":"Amazon.DynamoDBv2.AmazonDynamoDBException: 3 validation errors detected: Value null at 'transactItems.1.member.update.updateExpression' failed to satisfy constraint: Member must not be null; Value null at 'transactItems.2.member.update.updateExpression' failed to satisfy constraint: Member must not be null; Value null at 'transactItems.3.member.update.updateExpression' failed to satisfy constraint: Member must not be null
---> Amazon.Runtime.Internal.HttpErrorResponseException: Exception of type 'Amazon.Runtime.Internal.HttpErrorResponseException' was thrown.
at Amazon.Runtime.HttpWebRequestMessage.GetResponseAsync(CancellationToken cancellationToken)
at Amazon.Runtime.Internal.HttpHandler`1.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.Unmarshaller.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.ErrorHandler.InvokeAsync[T](IExecutionContext executionContext)
--- End of inner exception stack trace ---
at Amazon.Runtime.Internal.HttpErrorResponseExceptionHandler.HandleExceptionStream(IRequestContext requestContext, IWebResponseData httpErrorResponse, HttpErrorResponseException exception, Stream responseStream)
at Amazon.Runtime.Internal.HttpErrorResponseExceptionHandler.HandleExceptionAsync(IExecutionContext executionContext, HttpErrorResponseException exception)
at Amazon.Runtime.Internal.ExceptionHandler`1.HandleAsync(IExecutionContext executionContext, Exception exception)
at Amazon.Runtime.Internal.ErrorHandler.ProcessExceptionAsync(IExecutionContext executionContext, Exception exception)
at Amazon.Runtime.Internal.ErrorHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.Signer.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.CredentialsRetriever.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.ErrorCallbackHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.Runtime.Internal.MetricsHandler.InvokeAsync[T](IExecutionContext executionContext)
at Amazon.DynamoDBv2.DocumentModel.MultiTransactWrite.WriteItemsHelperAsync(CancellationToken cancellationToken)
at Amazon.DynamoDBv2.DocumentModel.MultiTableDocumentTransactWrite.ExecuteHelperAsync(CancellationToken cancellationToken)
at Amazon.DynamoDBv2.DataModel.MultiTableTransactWrite.ExecuteHelperAsync(CancellationToken cancellationToken)
Cause: ToUpdateTransactWriteRequestItem.GetRequest()
removes the keys from the request which results in a null update expression per the stack trace
//ToUpdateTransactWriteRequestItem
public TransactWriteItem GetRequest()
{
TransactWriteItemOperationConfig itemOperationConfig = this.OperationConfig ?? new TransactWriteItemOperationConfig();
Update request = new Update()
{
Key = (Dictionary<string, AttributeValue>) this.Key,
TableName = this.TransactionPart.TargetTable.TableName,
ReturnValuesOnConditionCheckFailure = (Amazon.DynamoDBv2.ReturnValuesOnConditionCheckFailure) EnumMapper.Convert(itemOperationConfig.ReturnValuesOnConditionCheckFailure)
};
Dictionary<string, AttributeValueUpdate> attributeUpdateMap = this.TransactionPart.TargetTable.ToAttributeUpdateMap(this.Document, !this.TransactionPart.TargetTable.HaveKeysChanged(this.Document));
//Keys are removed
foreach (string keyName in this.TransactionPart.TargetTable.KeyNames)
attributeUpdateMap.Remove(keyName);
itemOperationConfig.ConditionalExpression?.ApplyExpression(request, this.TransactionPart.TargetTable);
//Condition is false
if (attributeUpdateMap.Any<KeyValuePair<string, AttributeValueUpdate>>())
{
string statement;
Dictionary<string, AttributeValue> expressionAttributeValues;
Dictionary<string, string> expressionAttributes;
//Statement is never generated --> Exception
Common.ConvertAttributeUpdatesToUpdateExpression(attributeUpdateMap, out statement, out expressionAttributeValues, out expressionAttributes);
request.UpdateExpression = statement;
if (request.ExpressionAttributeValues == null)
{
request.ExpressionAttributeValues = expressionAttributeValues;
}
else
{
foreach (KeyValuePair<string, AttributeValue> keyValuePair in expressionAttributeValues)
request.ExpressionAttributeValues.Add(keyValuePair.Key, keyValuePair.Value);
}
if (request.ExpressionAttributeNames == null)
{
request.ExpressionAttributeNames = expressionAttributes;
}
else
{
foreach (KeyValuePair<string, string> keyValuePair in expressionAttributes)
request.ExpressionAttributeNames.Add(keyValuePair.Key, keyValuePair.Value);
}
}
return new TransactWriteItem() { Update = request };
}
Reproduction Steps
//Given
public sealed record Table1 //Same for TTable2 and TTable3
{
[DynamoDBHashKey("Id")]
public string Id { get; set; } = string.Empty;
[DynamoDBRangeKey("RelatedId")]
public string RelatedId { get; set; } = string.Empty;
}
//When
public static async Task TransactWriteWithRetryAsync<TTable1, TTable2, TTable3>(this IDynamoDBContext dbContext, TTable1 table1, TTable2 table2, TTable3 table3)
{
TransactWrite<TTable1> t1 = dbContext.CreateTransactWrite<TTable1>(TransactionDynamoDbOperationConfig);
t1.AddSaveItem(table1);
TransactWrite<TTable2> t2 = dbContext.CreateTransactWrite<TTable2>(TransactionDynamoDbOperationConfig);
t2.AddSaveItem(table2);
TransactWrite<TTable3> t3 = dbContext.CreateTransactWrite<TTable3>(TransactionDynamoDbOperationConfig);
t3.AddSaveItem(table3);
MultiTableTransactWrite transaction = dbContext.CreateMultiTableTransactWrite(t1, t2, t3);
//Throws
await DynamoDbRetryPolicy.ExecuteAsync(() => transaction.ExecuteAsync());
}
Possible Solution
Solution: Generate update expression in ToUpdateTransactWriteRequestItem.GetRequest() when only partition and sort keys are present.
Work-Around: Add a dummy property to the model representing the table.
//Example workaround
public sealed record Table1
{
[DynamoDBHashKey("Id")]
public string Id { get; set; } = string.Empty;
[DynamoDBRangeKey("RelatedId")]
public string RelatedId { get; set; } = string.Empty;
/// <remarks>DO NOT USE. Exists only as a workaround. Object persistence model transactions needs properties to exists on the objects other than the keys</remarks>
[DynamoDBProperty, JsonIgnore]
public DateTime DateTime { get; set; } = DateTime.MinValue;
}
Additional Information/Context
No response
AWS .NET SDK and/or Package version used
3.7.203.13
Targeted .NET Platform
.NET 7
Operating System and version
MacOS 13.6.1 (22G313)