REST Services
Invoices, contracts and reports are often requested by other systems rather than created where a user edits them: an order system needs a PDF invoice, a portal a filled contract, a mobile app a report. An HTTP service built with ASP.NET Core and TX Text Control .NET Server gives all of them one place to create, merge and convert documents, whatever language they are written in.
The service runs ServerTextControl and MailMerge in its own process and needs only the engine package, TXTextControl.TextControl.Core.SDK; no backend process and no Document Editor packages are involved. The design of requests and responses comes first, followed by a merge endpoint, clients in .NET and JavaScript, error handling, long-running jobs and security.
Design Requests and Responses
A document service receives a template or a document and data, and returns a document. Decide early how both travel:
| Options | Use it when | |
|---|---|---|
| Documents in the request | Base64 in a JSON body | The client sends data and a template in one request, or the template comes from the Document Editor, whose saveDocument method returns base64 |
A file upload (multipart/form-data) | Users upload files from a form; Load Uploaded Documents in ASP.NET Core shows the endpoint | |
| Documents in the response | The file itself, with its content type and a file name | The client stores or downloads the document |
| Base64 in a JSON body | The client loads the document into the Document Editor or the Document Viewer |
Return files with the content type of their format:
| Format | Content type |
|---|---|
application/pdf | |
| DOCX | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
| HTML | text/html |
Return Converted Documents From ASP.NET Core shows a controller that returns one document in several formats.
Create a Merge Endpoint
The following Program.cs of an ASP.NET Core application maps a minimal API endpoint that merges JSON data into a DOCX template and returns a PDF:
using TXTextControl;
using TXTextControl.DocumentServer;
using TXTextControl.DocumentServer.Json;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
app.MapPost("/api/documents/merge", (MergeRequest request) =>
{
byte[] template;
try
{
template = Convert.FromBase64String(request.Template);
}
catch (FormatException)
{
return Results.BadRequest("The template isn't valid base64 data.");
}
using (ServerTextControl tx = new ServerTextControl())
{
tx.Create();
try
{
// Import merge fields and merge blocks of Word templates
tx.Load(template, BinaryStreamType.WordprocessingML, new LoadSettings
{
ApplicationFieldFormat = ApplicationFieldFormat.MSWord,
LoadSubTextParts = true
});
}
catch (FilterException exception)
{
return Results.BadRequest($"The template can't be loaded: {exception.Reason}");
}
using (MailMerge mailMerge = new MailMerge { TextComponent = tx })
{
try
{
mailMerge.MergeJsonData(request.MergeData);
}
catch (ParserException)
{
return Results.BadRequest("The merge data isn't valid JSON.");
}
}
tx.Save(out byte[] pdf, BinaryStreamType.AdobePDF);
return Results.File(pdf, "application/pdf", "document.pdf");
}
});
app.Run();
public record MergeRequest(string Template, string MergeData);
The code works as follows:
MapPostmaps the endpoint, and ASP.NET Core reads the JSON body{ "template": "...", "mergeData": "..." }into theMergeRequestrecord.- ServerTextControl.Load loads the template. LoadSettings.ApplicationFieldFormat set to
MSWordand LoadSubTextParts import the merge fields and merge blocks of the Word template. - MailMerge.MergeJsonData merges the data. The JSON string contains a single object or an array of equally structured objects; an array creates one document with a copy of the template for each object.
- ServerTextControl.Save creates the PDF, and
Results.Filereturns it with its content type and file name. - Invalid input ends with status 400 and a message instead of an unhandled exception: a FilterException when the template can't be loaded, with the cause in Reason, and a ParserException when the JSON can't be parsed.
The blog article Designing a MailMerge Web API Endpoint with ASP.NET Core in C# adds merge and PDF settings to the request.
Call the Service From .NET
A .NET client, such as another web application, a background job or a desktop application, sends the template and the data with HttpClient:
using System.Text;
using System.Text.Json;
using HttpClient client = new HttpClient();
string json = JsonSerializer.Serialize(new
{
template = Convert.ToBase64String(File.ReadAllBytes("template.docx")),
mergeData = File.ReadAllText("data.json")
});
HttpResponseMessage response = await client.PostAsync(
"https://localhost:7241/api/documents/merge",
new StringContent(json, Encoding.UTF8, "application/json"));
if (response.IsSuccessStatusCode)
{
File.WriteAllBytes("document.pdf", await response.Content.ReadAsByteArrayAsync());
}
else
{
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
The client needs no TX Text Control package. It writes the PDF to document.pdf, or prints the message of the service when the request fails. Replace the address with the address of your service.
Call the Service From the Browser
A page with the Document Editor can send the template a user edits and download the merged PDF:
function mergeToPdf(data) {
TXTextControl.saveDocument(TXTextControl.StreamType.WordprocessingML, async function (result) {
const response = await fetch("https://localhost:7241/api/documents/merge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template: result.data, mergeData: JSON.stringify(data) })
});
if (!response.ok) {
alert(await response.text());
return;
}
// Offer the PDF as a download
const url = URL.createObjectURL(await response.blob());
const link = document.createElement("a");
link.href = url;
link.download = "document.pdf";
link.click();
URL.revokeObjectURL(url);
});
}
The code works as follows:
- TXTextControl.saveDocument saves the document of the editor as DOCX and passes it base64-encoded in SaveDocumentResult.data.
fetchposts the template and the data as JSON. The data is an object of the page, serialized to the JSON string the service expects.- The response is a PDF file, which the page offers as a download through a temporary link.
When the page and the service run on different origins, the service must allow cross-origin requests from the page with a CORS policy, as Configure Program.cs for Separate Frontends shows. Angular applications call the same endpoint with HttpClient, as in the blog article MailMerge with Angular and ASP.NET Core.
Return Meaningful Errors
Clients can only react to a failure they can recognize. Map the problems of a document service to status codes:
| Problem | Status code |
|---|---|
Missing input or data that isn't valid base64 (FormatException) | 400 Bad Request |
A template that can't be loaded (FilterException) or JSON that can't be parsed (ParserException) | 400 Bad Request |
| A requested template or data file that doesn't exist | 404 Not Found |
| An unexpected error while reading or writing files | 500 Internal Server Error |
FilterException.FilterError names the cause of a filter error, such as WrongFileType, Encrypted or InvalidPassword. A LicenseLevelException means that a feature isn't part of the licensed product level. Validate all input before you use it, and use Path.GetFileName when a client names a file, so that a request can't reach files outside the intended folder.
Keep the Service Responsive
An HTTP request should return without a noticeable delay. A typical invoice is created in a few hundred milliseconds and can be returned directly. Documents with many nested merge blocks and hundreds or thousands of pages take seconds or minutes and need a different approach:
- Accept the request and create the document later. The service stores the request in a queue and returns immediately. A background worker, an
IHostedService, creates the documents one after another, so the endpoints stay responsive. When a document is ready, the service calls a webhook of the client, which then downloads the document. The blog article Queuing and Generating Documents using an IHostedService and WebHooks implements this pattern. - Merge large batches in parallel processes. The
TXTextControl.DocumentServer.ParallelMailMergepackage runs merge jobs in separate worker processes; Use ParallelMailMerge in ASP.NET Core registers it in a web service. - Measure the capacity of your service. How many documents a service creates in parallel depends on the documents, the processor cores and the memory. Limit the parallel work of the service to what load tests of your own templates and data show, as Measure Your Own Workload describes.
- Run the service apart from the Document Editor. Heavy document processing can block the WebSocket connections of editors in the same application; see Separate Processing From Editing.
Secure the Service
A document service works with templates and often with personal data, so it must not be open to everyone:
- Authenticate the callers. Put authentication and authorization in front of every endpoint that creates documents. Authentication covers the options.
- Keep tokens out of URLs. Tokens in query strings end up in the logs of web servers, proxies and monitoring tools. See Access Tokens.
- Validate the input. Check templates, data and file names before you process them, to prevent injection attacks and access to unintended files.
Consider DS Server
With TX Text Control .NET Server, you build the Web API yourself, including security, authentication and document handling, and you get full access to the API. DS Server, a separate Text Control product built on the same engine, provides ready-made document processing endpoints with OAuth authentication that any language can call. Introduction compares the Text Control products.
Next Steps
- ASP.NET Core: services and dependency injection for
ServerTextControl. - Mail Merge and Reporting: templates, merge fields and data sources in depth.
- Saving and Converting Documents: the formats and settings of the documents a service returns.
- Deployment Guide: run the service on Windows, Linux, Docker and Azure.