Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
JCKortlang opened this issue Nov 7, 2023 · 3 comments
Labels
bug This issue is a bug. dynamodb p2 This is a standard priority issue queued

Comments

@JCKortlang
Copy link

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)

@JCKortlang JCKortlang added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Nov 7, 2023
@ashishdhingra ashishdhingra added dynamodb needs-reproduction This issue needs reproduction. and removed needs-triage This issue or PR still needs to be triaged. labels Nov 7, 2023
@ashishdhingra ashishdhingra self-assigned this Nov 7, 2023
@ashishdhingra
Copy link
Contributor

@JCKortlang Could you please share the following:

  • Exception you are getting with stack trace.
  • Are their any updates to Key attributes in your use case.
    • As rightly pointed out by you, ToUpdateTransactWriteRequestItem.GetRequest() removes the key attributes here. Hence, in your case if there is update to any of the key attributes, it would not generate UpdateExpression since it is removing keys and hence would error away.
  • Is it possible to share the complete code sample (which demonstrates fetch, update transact write, etc.) to reproduce the issue?

Thanks,
Ashish

@ashishdhingra ashishdhingra added the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Nov 8, 2023
@JCKortlang
Copy link
Author

JCKortlang commented Nov 9, 2023

All requested information is present in the issue? Can you please elaborate as to what is unclear?

//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;
}
public sealed record Table2
{
    [DynamoDBHashKey("Id")]
    public string Id { get; set; } = string.Empty;

    [DynamoDBRangeKey("RelatedId")]
    public string RelatedId { get; set; } = string.Empty;
}

//When
public static class Program
{
    // "Because versioning is not supported when using batch operations, you must explicitly disable versioning"
    // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DotNetDynamoDBContext.BatchOperations.html
    private static readonly DynamoDBOperationConfig TransactionDynamoDbOperationConfig = new()
    {
        SkipVersionCheck = true,
        ConditionalOperator = ConditionalOperatorValues.And
    };

    public static Task Main(string[] args)
    {
        var table1 = new Table1("Id0", "Id1");
        var table2 = new Table2("Id1", "Id0");

        //Assuming the context has been setup correctly to map to the tables
        IDynamoDbContext dbContext = new DynamoDbContext(new DynamoDbClient());

        TransactWrite<Table1> t1 = dbContext.CreateTransactWrite<Table1>(TransactionDynamoDbOperationConfig);
        t1.AddSaveItem(table1);

        TransactWrite<Table2> t2 = dbContext.CreateTransactWrite<Table2>(TransactionDynamoDbOperationConfig);
        t2.AddSaveItem(table2);

        MultiTableTransactWrite transaction = dbContext.CreateMultiTableTransactWrite(t1, t2);
        //Throws
        await transaction.ExecuteAsync();
    }
}

@github-actions github-actions bot removed the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Nov 10, 2023
@ashishdhingra
Copy link
Contributor

Reproducible using below code:

static async Task TestTransactWrite()
{
    DynamoDBOperationConfig TransactionDynamoDbOperationConfig = new()
    {
        SkipVersionCheck = true,
        ConditionalOperator = ConditionalOperatorValues.And
    };

    var table1 = new Table1() { Id = "Id0", RelatedId = "Id1" };
    var table2 = new Table2() { Id = "Id1", RelatedId = "Id0" };

    IDynamoDBContext dbContext = new DynamoDBContext(new AmazonDynamoDBClient());

    TransactWrite<Table1> t1 = dbContext.CreateTransactWrite<Table1>(TransactionDynamoDbOperationConfig);
    t1.AddSaveItem(table1);

    TransactWrite<Table2> t2 = dbContext.CreateTransactWrite<Table2>(TransactionDynamoDbOperationConfig);
    t2.AddSaveItem(table2);

    MultiTableTransactWrite transaction = dbContext.CreateMultiTableTransactWrite(t1, t2);
    await transaction.ExecuteAsync();
}

[DynamoDBTable("Table1")]
public class Table1
{
    [DynamoDBHashKey("Id")]
    public string Id { get; set; } = string.Empty;

    [DynamoDBRangeKey("RelatedId")]
    public string RelatedId { get; set; } = string.Empty;
}

[DynamoDBTable("Table2")]
public class Table2
{
    [DynamoDBHashKey("Id")]
    public string Id { get; set; } = string.Empty;

    [DynamoDBRangeKey("RelatedId")]
    public string RelatedId { get; set; } = string.Empty;
}

Needs review with the team.

@ashishdhingra ashishdhingra added needs-review p2 This is a standard priority issue and removed needs-reproduction This issue needs reproduction. labels Nov 14, 2023
@ashishdhingra ashishdhingra removed their assignment Nov 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This issue is a bug. dynamodb p2 This is a standard priority issue queued
Projects
None yet
Development

No branches or pull requests

2 participants