Skip to content

TransactWrite<T>.AddSaveItem throws when T does NOT contain DynamoDbProperty Attributes #3095

Closed
@JCKortlang

Description

@JCKortlang

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue is a bug.dynamodbp2This is a standard priority issuequeued

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions