Asynchronous File Conversion Using Polling in C#

Tomas, CEO

Using the ConvertAPI service in a .NET project is a great way to convert documents and files from one format to another. In this blog post, we will explore how we can perform an asynchronous file conversion using this service.

What is asynchronous REST API?

An asynchronous REST API is not fundamentally different from a typical REST API because the REST architectural style does not deal with concurrency and asynchrony. But, generally, when we talk about "asynchronous REST APIs", we're discussing leveraging asynchronous features of the underlying HTTP protocol and/or the programming language used to improve the performance and scalability of the API. Usually, when we interact with REST APIs, we send a request and wait for the response. This is a synchronous interaction because the client has to wait for the server to finish processing the request and send a response back. The client is blocked during this period and cannot do anything else. However, in an asynchronous REST API, the client does not wait for the server to complete processing before moving on to another task. Once the client sends a request to the server, it can perform other tasks while waiting for the server's response.

Let's see an example: a client requests the server to start a long-running task, like converting a large file using ConvertAPI. Instead of making the client wait for the task to finish, the server quickly responds with a Job ID. The client can then periodically check the status of the job by sending requests with that Job ID, or if an appropriate callback mechanism is implemented, the server can notify the client when the task is complete. This allows the client to do other work while the server is processing the task, and we can think of this as an example of an "asynchronous REST API".

It is worth mentioning that the term "asynchronous REST API" is more about how we design and use the API rather than any particular feature of REST itself.

The Implementation

To carry out the task, we'll use HTTPClient, a library used to send HTTP requests and receive HTTP responses from a URI. This modern HTTP client is practical and flexible, perfectly fitting for different types of operations. We will implement the following:

  • Submitting a file to ConvertAPI.
  • Polling the API until we get the result.

Let's say for example, we have a docx file which we want to convert to a pdf. For this, we use the SubmitFileForConversion method where we pass the name of the file we want to be converted.

const string inputFile = "SampleDOCFile_1000kb.docx";
var fileConverter = new FileConverter(new HttpClient(), secret);
var jobId = await fileConverter.SubmitFileForConversion(inputFile);

Here, FileConverter is our custom class where we handle all the operations of ConvertAPI. The SubmitFileForConversion method creates a POST request to the ConvertAPI service. We must include the file we want to convert as part of the multipart/form-data in the request.

If everything goes well, this method will return a 'Job Id' which we can use to track the status of our conversion request.

public async Task<string> SubmitFileForConversion(string inputFile)

Now that we have submitted our file for conversion and received a Job Id, we need to continually poll the ConvertAPI service to know when our file is converted. For this, we use another method from the FileConverter class: WaitForConversion. We pass the Job Id received from SubmitFileForConversion and the name under which we want to save our output file.

var httpStatusCode = HttpStatusCode.Accepted;
while (httpStatusCode == HttpStatusCode.Accepted)
{
    httpStatusCode = await fileConverter.WaitForConversion(jobId, outputFile);
    if (httpStatusCode == HttpStatusCode.Accepted)
    {
        await Task.Delay(5000);   
    }
}

In WaitForConversion, we send a GET request to the ConvertAPI service with our Job Id in the URL. ConvertAPI service will respond with HttpStatusCode.OK if the conversion has finished. If the conversion is still in progress, it will respond with HttpStatusCode.Accepted and we continue polling every 5000 milliseconds (5 seconds) until the conversion finishes.

public async Task<HttpStatusCode> WaitForConversion(string jobId, string outputFile)

When the conversion finishes, ConvertAPI sends us the converted file which we need to save. For this, we use the SaveConvertedFile method where we pass the response from ConvertAPI and the name under which we want to save the file.

private async Task SaveConvertedFile(HttpResponseMessage response, string outputFile)

And that's it! Note that before you can use this code, you must register with ConvertAPI to get a secret key. This secret key should be passed when creating the FileConverter object.

Remember to add the necessary using directives at the top of your C# file:

using System.Net;
using System.Net.Http.Headers;
using System.Web;

If you are using .NET Core, you might need to install System.Web.HttpUtility via NuGet package manager. Enjoy your coding!

Note: Don't forget to responsibly handle any exception that could occur when dealing with file I/O operations or in-network requests.

The complete C# code is given below, or it can also be downloaded from the GitHub repository: https://github.com/ConvertAPI/convertapi-example-dotnet/tree/main/ConvertAPI.Example.DotNet/AsyncConversion.

using System.Net;
using System.Net.Http.Headers;
using System.Web;

const string secret = "";
const string inputFile = "SampleDOCFile_1000kb.docx";
var outputFile = Path.Combine(Path.GetTempPath(), "output.pdf");

var fileConverter = new FileConverter(new HttpClient(), secret);
var jobId = await fileConverter.SubmitFileForConversion(inputFile);

var httpStatusCode = HttpStatusCode.Accepted;
while (httpStatusCode == HttpStatusCode.Accepted)
{
    httpStatusCode = await fileConverter.WaitForConversion(jobId, outputFile);
    if (httpStatusCode == HttpStatusCode.Accepted)
    {
        await Task.Delay(5000);   
    }
}


Console.WriteLine("File converted.");
Console.ReadLine();

public class FileConverter
{
    private readonly string _secret;
    private readonly HttpClient _client;

    public FileConverter(HttpClient client, string secret)
    {
        _secret = secret;
        _client = client;
    }

    public async Task<string> SubmitFileForConversion(string inputFile)
    {
        if (string.IsNullOrEmpty(_secret))
            throw new ArgumentException("The secret is missing, get one for free at https://www.convertapi.com/a/auth", nameof(_secret));
        
        using var fs = new FileStream(inputFile, FileMode.Open, FileAccess.Read);
        var convertBuilder = new UriBuilder("https://v2.convertapi.com/async/convert/docx/to/pdf");
        var convertQuery = HttpUtility.ParseQueryString(convertBuilder.Query);
        convertQuery["Secret"] = _secret;
        convertBuilder.Query = convertQuery.ToString();

        var streamContent = new StreamContent(fs);
        streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
        {
            FileName = Path.GetFileName(inputFile)
        };

        var convertResponse = await _client.PostAsync(convertBuilder.Uri, streamContent);

        if (convertResponse.StatusCode != HttpStatusCode.OK) return null;

        var responseBody = await convertResponse.Content.ReadAsStringAsync();
        var asyncResponse = System.Text.Json.JsonSerializer.Deserialize<AsyncResponse>(responseBody);
        Console.WriteLine($"The Word document has been sent for conversion. The task is identified by the Async ID: {asyncResponse.JobId}. Now, we are awaiting the result.");
        return asyncResponse.JobId;
    }

    public async Task<HttpStatusCode> WaitForConversion(string jobId, string outputFile)
    {
        var resultResponse = await _client.GetAsync($"https://v2.convertapi.com/async/job/{jobId}");

        switch (resultResponse.StatusCode)
        {
            case HttpStatusCode.OK:
                await SaveConvertedFile(resultResponse, outputFile);
                break;

            case HttpStatusCode.Accepted:
                Console.WriteLine("File conversion is in progress...");
                break;

            default:
                Console.WriteLine("An error occurred");
                var message = await resultResponse.Content.ReadAsStringAsync();
                Console.WriteLine(message);
                break;
        }

        return resultResponse.StatusCode;
    }

    private async Task SaveConvertedFile(HttpResponseMessage response, string outputFile)
    {
        var resultFile = await response.Content.ReadAsStreamAsync();

        await using var outputFileStream = new FileStream(outputFile, FileMode.Create);
        await resultFile.CopyToAsync(outputFileStream);
        Console.WriteLine($"File conversion completed. The file saved to {outputFile}");
    }
}

public class AsyncResponse
{
    public string JobId { get; set; }
}