Skip to content

RequestId in ServiceContext Does Not Prevent Duplicate Creation on Retry #352

@alex-zinich

Description

@alex-zinich

I'm trying to prevent duplicate entity creation when retrying failed requests due to transient issues like timeouts. I understand that a timeout may result in the client retrying the request, which could inadvertently create duplicate records.

I came across the RequestId property in ServiceContext and started experimenting to see if it could help ensure idempotency. Here's what I tested:

  1. I created a ServiceContext and assigned a unique RequestId to it.
  2. I created an Invoice and added it using DataService.Add().
  3. Then, I created a Payment linked to that invoice and called DataService.Add(payment) twice, using the same RequestId.
    Result: Two identical payments were created. No error or warning was thrown.

Next, I tried using a Batch:

  1. I set the same RequestId in the ServiceContext.
  2. I created a new batch with dataService.CreateNewBatch(), added the same Payment once, and called Batch.Execute() twice.
    Again, the result was two identical payments created.

So based on these tests, it seems like setting RequestId in ServiceContext does not prevent duplicate operations during retries.
Is there something I’m missing, or is RequestId not actually used by the QuickBooks Online API/SDK to ensure idempotency for these kinds of operations?

Here’s the relevant code for context:

Invoice invoice = new()
{
    DocNumber = $"POS-a520-6e27de50cdпf",
    CustomerRef = new ReferenceType { Value = "1", name = "Adwin Ko" },
    TxnDate = new DateTime(2025, 4, 10),
    DueDate = new DateTime(2025, 4, 10),
    DueDateSpecified = true,
    TxnTaxDetail = new TxnTaxDetail
    {
        TotalTax = Convert.ToDecimal(10),
        TotalTaxSpecified = true
    },
    Line = new[]
    {
        new Line
        {
            Description = "Description",
            Amount = 100,
            AmountSpecified = true,
            DetailType = LineDetailTypeEnum.DescriptionOnly,
            DetailTypeSpecified = true,
        }
    }
};

DataService dataService = new(_serviceContext);
Invoice createdInvoice = dataService.Add(invoice);

var payment = new Payment
{
    CustomerRef = new ReferenceType { Value = "1" },
    TotalAmt = 10,
    TotalAmtSpecified = true,
    TxnDate = DateTime.UtcNow,
    PrivateNote = "POS-a520-6e27de50cdgg",
    PaymentRefNum = "POS-a520-6e27de50cdgg",
    Line = new[]
    {
        new Line
        {
            Amount = 10,
            AmountSpecified = true,
            LinkedTxn = new[]
            {
                new LinkedTxn
                {
                    TxnId = createdInvoice.Id,
                    TxnType = "Invoice"
                }
            }
        }
    }
};

_serviceContext.RequestId = Guid.NewGuid().ToString();

// Direct call (duplicated)
dataService.Add(payment);
dataService.Add(payment);

// Using Batch (duplicated)
Batch batch = dataService.CreateNewBatch();
batch.Add(payment, "id", OperationEnum.create);
batch.Execute();
batch.Execute();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions