A very popular and useful way to access document processing functionality from a variety of applications and clients is to implement a Web API to create documents. Document generation can be a complex and time-consuming task, depending on the size and data structure of the template.

This task can take several seconds or even minutes when creating documents with many nested merge blocks and 100s or 1000s of pages. A typical document such as an invoice (which takes about 300 ms) could be created on the fly and returned by the HTTP request immediately after generation. However, requests that are longer than that (in the range of seconds) should be handled in a different way.

Implementing WebHooks

Creating a RESTful Web API that is called with a WebHook URL that receives a notification with a download link when the document is successfully created is one way to solve this problem.

WebHooks

How to design and implement this is discussed in another article. This article discusses the implementation of the WebHook and the invocation of the asynchronous Web API.

Learn More

This sample shows how to build an asynchronous, RESTful Web API that calls a WebHook after the document generation process is terminated.

Generating Documents using a RESTful, Asynchronous Web API using WebHooks

In this article, we are going to add another layer to this concept. Document generation requests are queued in a database and a background task using IHostedService is scheduled to generate documents.

IHostedService Background Tasks

Background tasks are application elements that work behind the scenes without end-user input or interaction. They typically involve processes that take a long time to complete or require the host application to keep updating parameters. In other words, it is the perfect application for time-consuming document creation tasks.

The client sends an HttpPost request to an endpoint that immediately returns a positive response if the request is acceptable. This request does not generate the document, but places the request in a queue.

[Route("api/[controller]/merge")]
[HttpPost]
public ProcessingRequest Post(string webHookUrl) {
ProcessingRequest request = new ProcessingRequest() {
WebHookUrl = webHookUrl
};
request.Create(Url.ActionLink("Get", "DocumentProcessing", new { id = "1" }));
return request;
}
view raw test.cs hosted with ❤ by GitHub

The following code creates a unique URL where the document can be retrieved after it is created, and places the request in the queue.

public void Create(string requestUrl) {
this.Id = Guid.NewGuid().ToString();
this.RetrieveDocumentUrl = requestUrl.Replace("/1", "/" + this.Id);
using (var db = new LiteDatabase(@"Filename=App_Data/processingqueue.db; Connection=shared")) {
var col = db.GetCollection<ProcessingRequest>("queue");
col.Insert(this);
}
}
view raw test.cs hosted with ❤ by GitHub

We implement the BackgroundGenerator class, which is inherited from IHostedService.

public class BackgroundGenerator : IHostedService
{
private readonly ILogger<BackgroundGenerator> logger;
private readonly IWorker worker;
public BackgroundGenerator(ILogger<BackgroundGenerator> logger,
IWorker worker)
{
this.logger = logger;
this.worker = worker;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await worker.DoWork(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Generator worker stopping");
return Task.CompletedTask;
}
}
view raw test.cs hosted with ❤ by GitHub

In the Worker class, the DoWork method looks for requests in the queue and processes existing requests. The worker waits one second before the next round if no requests are found in the queue.

public async Task DoWork(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
ProcessingRequest request = new ProcessingRequest(null);
if (request.Id == null)
{
logger.LogInformation("No request found in queue");
await Task.Delay(1000, cancellationToken);
continue;
}
else
{
TextControlProcessing.Merge(request);
logger.LogInformation("Request found - executing worker");
}
}
}
view raw test.cs hosted with ❤ by GitHub

Once the document is created, the resulting file is stored in a database and a request is made to the WebHookUrl endpoint specified by the original client request. Basically, the request tells the consumer where to download the document once creation is complete.

public static void Merge(ProcessingRequest request) {
using (TXTextControl.ServerTextControl tx = new TXTextControl.ServerTextControl()) {
tx.Create();
// create document or use MailMerge to generate larger document
tx.Text = "My created document.";
byte[] data;
tx.Save(out data, TXTextControl.BinaryStreamType.AdobePDF);
request.StoreDocument(data);
}
request.Processed = true;
request.Update();
Task.Run(() => FireAndForgetWebHook(request));
}
private static void FireAndForgetWebHook(ProcessingRequest request) {
HttpClient client = new HttpClient();
var json = JsonConvert.SerializeObject(request);
var data = new StringContent(json, Encoding.UTF8, "application/json");
client.PostAsync(request.WebHookUrl, data);
}
view raw test.cs hosted with ❤ by GitHub

The WebHook endpoint must be implemented by the calling client application and retrieves a URL where the completed document can be downloaded.

[HttpPost]
public bool WebHook([FromBody] object request) {
dynamic ProcessingRequest = JObject.Parse(request.ToString());
HttpClient client = new HttpClient();
HttpResponseMessage responseMessage = client.GetAsync(ProcessingRequest.RetrieveDocumentUrl.Value).Result;
if (responseMessage.IsSuccessStatusCode) {
string data = responseMessage.Content.ReadAsStringAsync().Result;
System.IO.File.WriteAllBytes("App_Data/" + ProcessingRequest.Id.Value + ".pdf", Convert.FromBase64String(data));
return true;
}
else
return true;
}
view raw test.cs hosted with ❤ by GitHub

This endpoint will retrieve the document from the database on the basis of the given ID and will return the document.

[Route("api/[controller]/{id}")]
[HttpGet]
public string Get([FromRoute] string id) {
ProcessingRequest request = new ProcessingRequest(id);
return request.RetrieveDocument();
}
view raw test.cs hosted with ❤ by GitHub

The Sample Setup

In order to demonstrate this concept, the sample solution is made up of two separate projects:

  • tx_wp_api
    The asynchronous Web API with the Background Worker.
  • tx_api_consumer
    A consuming application that calls the Web API and implements the WebHook.

For demonstration purposes, the solution has two initial projects. After compiling and starting the application, two browser windows are opened.

One browser will open the Web API with Swagger support, and the other browser that is open will display the consumer application. The consumer application consists of nothing more than a button that makes a call to the Web API using an AJAX HttpGet request.

WebHooks

In the following screenshot you can see three windows, the consuming application, the output folder where the PDFs are stored, and the .NET console with the status logs.

WebHooks

Each time the user clicks the button, the request is added to the queue, and the background worker picks up requests from the queue and returns the PDF, which is then stored locally.

The Web API application and its responsiveness are not affected by the actual document generation.