I was recently working on a document e-signing solution that had me integrating with PandaDoc. The whole process was very straightforward, but what I liked the most was that I found out about a new .NET API that I had not seen before!
The two main steps in the process were uploading a PDF with placeholders (called Field Tags) to PandaDoc and then sharing the document with a customer (with the placeholders being converted into fillable form fields by then). Seems simple, and it was, but take a look at the documentation:
Between the two main points I mentioned above, there is another little step: "Wait for the Document to enter draft status". It's not surprising, really - it would actually be surprising if the document was ready the moment you upload it - what would I be paying for, then? 😁 There are two options here - you can either:
- set up a webhook listener, and the document status changes will be sent to you
- actively poll the document status endpoint until the desired status is reached
I would usually prefer the first option, but in my case, it did not make much sense to go that way without a major rewrite of a few processes, so I decided to go with active polling.
In my proof-of-concept implementation, I used Task.Delay
to introduce some delay between the requests to avoid being blocked by PandaDoc.
var documentCreationOptions = new DocumentCreationOptions(...);
var (documentId, documentStatus) = await _pandaDocClient.CreateDocument(file, fileName, documentCreationOptions);
while (status != "document.draft")
{
await Task.Delay(PollingInterval);
documentStatus = await _pandaDocClient.GetDocumentStatus(documentId);
if (status != "document.uploaded" && status != "document.draft")
{
throw new InvalidDocumentStatusException($"Invalid document status. DocumentId: {documentId}, Status: {documentStatus}.")
}
}
await _pandaDocClient.SendDocument(documentId);
It worked, but when I was cleaning up the solution and moving it to the main project, I said to myself: there should be a nicer way to do this - it's 2022, after all.
A quick Google search revealed what I was looking for:
The project was just migrated to .NET 6 so I got lucky, the cleaned-up version is not very different, but I like it much more:
var documentCreationOptions = new DocumentCreationOptions(...);
var (documentId, documentStatus) = await _pandaDocClient.CreateDocument(file, fileName, documentCreationOptions);
using (var timer = new PeriodicTimer(PollingInterval))
{
while (status != "document.draft" && await timer.WaitForNextTickAsync())
{
documentStatus = await _pandaDocClient.GetDocumentStatus(documentId);
if (status != "document.uploaded" && status != "document.draft")
{
throw new InvalidDocumentStatusException($"Invalid document status. DocumentId: {documentId}, Status: {documentStatus}.")
}
}
}
await _pandaDocClient.SendDocument(documentId);
So what's to like here? At first glance, all it did was introduce another nesting level for the using
statement. Well, that one can be easily fixed by moving the waiting part to a separate method and converting the using
statement to a using
declaration.
So under the hood, both approaches use the TimerQueueTimer
class. The difference is that the new PeriodicTimer
allocated just one TimerQueueTimer
instance while each Task.Delay()
creates a new object that later needs to be cleaned up too. This might not seem like a huge gain - especially here, since the wait time for the document state change should not be too long. But in a large system, those allocations might pile up, so it's always nice to make less of them when possible.
The PeriodicTimer
is not a silver bullet and in some cases, Task.Delay
will be the better option. With the new timer, the time between one operation finishing and the next one starting will be at most what you set as the interval. With Task.Delay
, the time will be more or less what you choose. Here's a little diagram to help visualize it:
So, as usual, use the tool that makes the most sense in your situation.
I know that some people will come and say that I could've used the Timer
class and be done with it. My first answer would be - which one? On a more serious note, the one thing that disqualified Timer
for me in this scenario is that Timer
callbacks can overlap, which does not make much sense when polling an external endpoint.
If you want more insight into how PeriodicTimer
came to be, head over to the GitHub issue where it all happened:
Cover photo by Renel Wackett on Unsplash