DOCX-to-PDF is the most common document conversion in .NET, and the right tool depends entirely on your deployment target. Interop needs Word installed — fine for an on-premises Windows server, impossible in a Linux container. Spire.Doc is cross-platform but the free tier caps at 3 pages. The ChangeThisFile API runs LibreOffice headless server-side so your container stays lean.
Method 1: Spire.Doc (cross-platform, no Word required)
Spire.Doc Free converts DOCX to PDF on Windows, Linux, and macOS. The free edition limits output to 3 pages; beyond that you need Spire.Doc (paid) or the API.
dotnet add package Spire.Doc
using Spire.Doc;
public static class DocxToPdf
{
/// <summary>Converts a DOCX file to PDF. Works on Linux/macOS without Word.</summary>
public static void Convert(string inputPath, string outputPath)
{
using var document = new Document();
document.LoadFromFile(inputPath);
document.SaveToFile(outputPath, FileFormat.PDF);
}
}
// Usage
DocxToPdf.Convert("report.docx", "report.pdf");
Console.WriteLine("Done");
Spire.Doc renders fonts, tables, images, headers, footers, and most Word styles. Complex tracked-changes documents may differ slightly from Word's rendering — check the output for high-stakes documents.
Method 2: Microsoft.Office.Interop.Word (highest fidelity, Windows only)
Interop drives a live Word instance via COM. Output is pixel-perfect because it IS Word — but Word must be installed, and COM objects must be released carefully or Word processes leak.
dotnet add package Microsoft.Office.Interop.Word
# Requires Word installed on the machine. Windows only.
using Microsoft.Office.Interop.Word;
using System.Runtime.InteropServices;
public static class DocxToPdfInterop
{
public static void Convert(string inputPath, string outputPath)
{
Application? wordApp = null;
Document? doc = null;
try
{
wordApp = new Application { Visible = false };
doc = wordApp.Documents.Open(
Path.GetFullPath(inputPath),
ReadOnly: true,
Visible: false);
doc.SaveAs2(
Path.GetFullPath(outputPath),
WdSaveFormat.wdFormatPDF);
}
finally
{
doc?.Close(WdSaveOptions.wdDoNotSaveChanges);
wordApp?.Quit();
if (doc != null) Marshal.ReleaseComObject(doc);
if (wordApp != null) Marshal.ReleaseComObject(wordApp);
}
}
}
Always release COM objects in a finally block. Leaked Word processes accumulate and eventually exhaust available memory. Do not use Interop in ASP.NET — it is not thread-safe and Microsoft explicitly does not support it in server scenarios.
Method 3: ChangeThisFile API (HttpClient, no Word or LibreOffice)
The API runs LibreOffice headless. Handles documents of any page count, runs on your existing HttpClient, no native deps. Free tier: 1,000 conversions/month.
# Verify with curl first
curl -X POST https://changethisfile.com/v1/convert \
-H "Authorization: Bearer ctf_sk_your_key" \
-F "file=@report.docx" \
-F "target=pdf" \
--output report.pdf
using System.Net.Http;
using System.Net.Http.Headers;
// Register in Program.cs:
// builder.Services.AddHttpClient("ctf",
// c => c.BaseAddress = new Uri("https://changethisfile.com"));
public class DocxToPdfService
{
private readonly HttpClient _http;
private const string ApiKey = "ctf_sk_your_key_here";
public DocxToPdfService(IHttpClientFactory factory)
=> _http = factory.CreateClient("ctf");
public async Task ConvertAsync(
string inputPath,
string outputPath,
CancellationToken ct = default)
{
await using var fileStream = File.OpenRead(inputPath);
using var form = new MultipartFormDataContent();
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document");
form.Add(fileContent, "file", Path.GetFileName(inputPath));
form.Add(new StringContent("pdf"), "target");
using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/convert")
{
Content = form,
Headers = { Authorization =
new AuthenticationHeaderValue("Bearer", ApiKey) }
};
using var response = await _http.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
await using var outStream = File.Create(outputPath);
await response.Content.CopyToAsync(outStream, ct);
}
}
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| Spire.Doc | Cross-platform, no Word, documents ≤3 pages (free) | Free tier page cap; paid license for longer docs |
| Office Interop | Pixel-perfect fidelity, Windows server with Word | Windows only, not thread-safe, COM leak risk |
| ChangeThisFile API | Linux containers, any page count, no native deps | Network call; 25MB file limit on free tier |
Production tips
- Never use Interop in ASP.NET. Microsoft explicitly states Office automation is not supported in server scenarios. Use Spire.Doc or the API for web applications.
- Use IHttpClientFactory. Register a named client once in
Program.cswith base address and timeout. InjectIHttpClientFactoryinto your service — avoids socket exhaustion from repeatednew HttpClient(). - Pass CancellationToken through. Wire
HttpContext.RequestAbortedso a cancelled HTTP request propagates to the file I/O and the API call, not just to the outer await. - Stream the upload. Open the file with
File.OpenReadand pass the stream toStreamContent. Don'tReadAllBytesinto memory for large documents. - Set a conversion timeout. Large DOCX files (many embedded images) can take 10–30 seconds through LibreOffice. Set
client.Timeout = TimeSpan.FromMinutes(2)in your named HttpClient config. - Font embedding matters. If your DOCX uses non-standard fonts and you're on Linux, LibreOffice may substitute them. Embed fonts in the DOCX before converting, or stick to standard fonts like Calibri, Arial, Times New Roman.
For Linux containers and web services, Spire.Doc (or the API for large docs) is the correct choice — Office Interop has no place in server-side .NET. For internal Windows tooling where Word is already present, Interop gives the highest fidelity. Free tier: 1,000 conversions/month.