Plugin Services
Since version 5.0, DS Server hands its own document processing and the open Document Editor sessions to plugins as two services: IDocumentProcessingService converts, merges and inspects documents, and IDocumentEditorSessionService works with the documents users have open in the Document Editor. A plugin controller receives them through ASP.NET Core dependency injection, without a request to the Web API and without an access token.
Read on for what each service offers and for a controller that uses it. The examples build on the plugin project of Creating a Plugin.
How the Services Work
DS Server registers both services in its dependency injection container. A plugin uses them the way any ASP.NET Core code uses a service:
- In a controller, as a constructor parameter, as in the examples below.
- In a middleware, with
httpContext.RequestServices.GetRequiredService<IDocumentProcessingService>(). - In
OnStart, withservices.GetService<IDocumentProcessingService>()on theIServiceProviderthat DS Server passes.
The interfaces are in the package TXTextControl.DocumentServices.Plugin.Abstractions 5.0.0 and later, in these namespaces:
| Namespace | Types |
|---|---|
TXTextControl.DocumentServices.DocumentProcessing.Abstractions | IDocumentProcessingService and its sub-APIs IPdfApi, IAcroFormsApi and IBarcodesApi |
TXTextControl.DocumentServices.DocumentProcessing.Models | The data of the processing service, such as MergeBody, DocumentInfo, PDFSettings, BarcodeSettings and the enumeration ReturnFormat |
TXTextControl.DocumentServices.DocumentProcessing | DocumentProcessingException, thrown when an error occurs during document processing |
TXTextControl.DocumentServices.DocumentEditor.Abstractions | IDocumentEditorSessionService, IDocumentEditorSession and the document objects of a session, such as ISelection and IApplicationField |
TXTextControl.DocumentServices.DocumentEditor.Options | LoadOptions and SaveOptions |
TXTextControl.DocumentServices.DocumentEditor.Enums | DocumentFormat and the other enumerations of a session |
TXTextControl.DocumentServices.Plugin.Abstractions.DocumentEditor.Models | SavedDocument, the result of saving a session |
Some type names, such as MergeBody and ReturnFormat, also exist in the client package TXTextControl.DocumentServices.DocumentProcessing. A plugin uses the types of the namespaces above. The package documentation lists all types: Plugin.Abstractions.
IDocumentProcessingService
IDocumentProcessingService offers the operations of the DocumentProcessing Web API as asynchronous methods. Each method takes the document as a byte array, or a request object such as MergeBody that contains it, and an optional CancellationToken:
| Method or property | What it does |
|---|---|
MergeAsync | Merges data into a template, given as a MergeBody with the template and the data as objects (MergeData) or as a JSON string (MergeDataJson). Returns one byte array per document, or one document when append is true |
ConvertAsync | Converts a document into a ReturnFormat: PDF, PDFA, DOC, DOCX, RTF, TX, TXT or HTML. An overload flattens form fields |
AppendAsync | Combines several documents, given as an AppendBody |
FindAndReplaceAsync | Replaces text in a document, given as a FindAndReplaceBody |
GetThumbnailsAsync | Creates images of the pages, with a zoom factor between 1 and 400 and an optional page range |
GetDocumentInfoAsync | Returns a DocumentInfo with the merge fields, merge blocks and document properties of a document |
PDF | The IPdfApi: CreateAsync creates a PDF or PDF/A document (PDFFormat) with PDFSettings, GetEmbeddedFilesAsync and GetMetaDataAsync read a PDF |
PDF.AcroForms | The IAcroFormsApi: GetFieldsAsync returns the form fields of a PDF |
Barcodes | The IBarcodesApi: CreateAsync creates a barcode image, and other methods return the default text, the text length limits and whether a text is valid for a BarcodeType |
The requests and results correspond to those of the Web API, described in Document Processing.
Merge a Template in a Plugin Controller
The following controller adds two endpoints: /plugin/reports/merge merges JSON data into a template and returns a PDF, and /plugin/reports/fields lists the merge fields of a template:
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using TXTextControl.DocumentServices.DocumentProcessing.Abstractions;
using TXTextControl.DocumentServices.DocumentProcessing.Models;
namespace ReportingPlugin;
[]
[]
public class ReportsController : ControllerBase
{
private readonly IDocumentProcessingService m_processing;
public ReportsController(IDocumentProcessingService processing)
{
m_processing = processing;
}
// Merges JSON data into a template and returns one PDF
[]
public async Task<IActionResult> Merge([FromBody] MergeRequest request)
{
MergeBody mergeBody = new()
{
Template = Convert.FromBase64String(request.Template),
MergeDataJson = request.Data.GetRawText()
};
byte[][] documents = await m_processing.MergeAsync(
mergeBody, ReturnFormat.PDF, true, HttpContext.RequestAborted);
return File(documents[0], "application/pdf", "report.pdf");
}
// Lists the merge fields of a template
[]
public async Task<IActionResult> Fields([FromBody] string template)
{
DocumentInfo info = await m_processing.GetDocumentInfoAsync(
Convert.FromBase64String(template), HttpContext.RequestAborted);
return Ok(info.MergeFields);
}
}
public record MergeRequest(string Template, JsonElement Data);
The code works as follows:
- The constructor receives
IDocumentProcessingServicefrom DS Server. Mergedecodes the Base64-encoded template and passes the JSON array of the request unchanged asMergeDataJson. The JSON must be an array of objects of the same type.MergeAsyncwithReturnFormat.PDFandtrueforappendmerges every record and appends the results to one document, which is the first element of the returned array.FieldscallsGetDocumentInfoAsyncand returns itsMergeFields, one entry per merge field with its name and text.HttpContext.RequestAbortedcancels the work when the client closes the connection.
A request to the merge endpoint sends the template, for example a DOCX or RTF file with the merge fields company_name and total, and one object per document:
{
"template": "<Base64-encoded template>",
"data": [
{ "company_name": "Text Control", "total": "1,250.00" },
{ "company_name": "Contoso", "total": "990.00" }
]
}
curl POST http://localhost:5000/plugin/reports/merge \
"Content-Type: application/json" \
@merge-request.json \
report.pdf
For the fields endpoint, the body is the Base64-encoded template as a JSON string. The result is a JSON array with one MergeField object per merge field, with the properties name, text, textBefore, textAfter, numericFormat, dateTimeFormat and preserveFormatting.
IDocumentEditorSessionService
IDocumentEditorSessionService returns the open Document Editor sessions of DS Server. Each Document Editor connected to DS Server has a session, identified by a connection ID:
| Member | What it does |
|---|---|
this[string connectionId], GetSession(string connectionId) | Return the session, or throw a KeyNotFoundException when there is no session with this ID |
TryGetSession(string connectionId, out IDocumentEditorSession session) | Returns false when there is no session with this ID |
An IDocumentEditorSession gives access to the document in the editor:
| Member | What it does |
|---|---|
ConnectionId | The connection ID of the session |
LoadAsync(LoadOptions) | Loads a document into the editor. LoadOptions.FromDocumentFormat creates the options from a DocumentFormat and the document as a string or a byte array |
SaveAsync(SaveOptions), SaveAsync() | Save the document in a DocumentFormat, by default in the internal format TX, and return a SavedDocument with Content and Format |
LoadJsonDatabaseAsync, LoadXmlDatabaseAsync, LoadDatabaseFromModelAsync and more | Load data for template design: the merge fields and blocks the user can insert |
Selection | The current selection, with methods such as GetTextAsync, SetTextAsync and ParagraphFormat.SetBackColorAsync |
ApplicationFields, FormFields, Paragraphs, TextFrames, HeadersAndFooters | The fields and other elements of the document |
SetEditModeAsync, SetUserNamesAsync, SetControlCharsAsync | Settings of the editor, each with a Get method |
DocumentFormat has the values TX, RTF, PlainText, HTML, DOCX, PDF, PDFA and XLSX.
Get the Connection ID
The Document Editor in the browser knows its connection ID: the connectionID property of the TXTextControl object. Read it once the editor is ready, with addEventListener, and send it to the code that calls the plugin:
TXTextControl.addEventListener("textControlLoaded", function () {
// Send the ID of this editor session to your application
fetch("/api/editor-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ connectionId: TXTextControl.connectionID })
});
});
The address /api/editor-session stands for an endpoint of your own application, which then calls the plugin endpoints below with the ID. A session exists only on the DS Server that opened it. With several servers behind a load balancer, see Multiple Servers.
Work With a Session in a Plugin Controller
The following controller loads HTML into a session, returns the selected text and returns the document as PDF:
using Microsoft.AspNetCore.Mvc;
using TXTextControl.DocumentServices.DocumentEditor.Abstractions;
using TXTextControl.DocumentServices.DocumentEditor.Enums;
using TXTextControl.DocumentServices.DocumentEditor.Options;
using TXTextControl.DocumentServices.Plugin.Abstractions.DocumentEditor.Models;
namespace ReportingPlugin;
[]
[]
public class EditorSessionsController : ControllerBase
{
private readonly IDocumentEditorSessionService m_sessions;
public EditorSessionsController(IDocumentEditorSessionService sessions)
{
m_sessions = sessions;
}
// Replaces the document in the editor with HTML
[]
public async Task<IActionResult> Load(string connectionId, [FromBody] string html)
{
if (!m_sessions.TryGetSession(connectionId, out IDocumentEditorSession? session))
{
return NotFound($"No Document Editor session with the ID '{connectionId}'.");
}
LoadOptions options = LoadOptions.FromDocumentFormat(DocumentFormat.HTML, html);
await session.LoadAsync(options, HttpContext.RequestAborted);
return NoContent();
}
// Returns the selected text
[]
public async Task<IActionResult> Selection(string connectionId)
{
if (!m_sessions.TryGetSession(connectionId, out IDocumentEditorSession? session))
{
return NotFound($"No Document Editor session with the ID '{connectionId}'.");
}
string text = await session.Selection.GetTextAsync(HttpContext.RequestAborted);
return Ok(text);
}
// Returns the document in the editor as PDF
[]
public async Task<IActionResult> Pdf(string connectionId)
{
if (!m_sessions.TryGetSession(connectionId, out IDocumentEditorSession? session))
{
return NotFound($"No Document Editor session with the ID '{connectionId}'.");
}
SavedDocument saved = await session.SaveAsync(
new SaveOptions(DocumentFormat.PDF), HttpContext.RequestAborted);
return File(saved.Content, "application/pdf", "document.pdf");
}
}
The code works as follows:
- The route contains the connection ID, for example
/plugin/editor/KOnY0Ci3TmlxW2z7lVTvB/pdf. TryGetSessionfinds the session. The controller answers 404 when the user has closed the editor or the ID belongs to another server.LoadOptions.FromDocumentFormatwithDocumentFormat.HTMLand a string creates the options for text formats; for DOCX or PDF, pass the bytes of the document instead.LoadAsyncreplaces the document the user sees.Selection.GetTextAsyncreturns the text the user has selected.SaveAsyncwithnew SaveOptions(DocumentFormat.PDF)returns aSavedDocumentwhoseContentholds the PDF.
Troubleshooting
IPdfApi.CreateAsync Throws "Nullable object must have a value."
In DS Server 5.0.2, PDF.CreateAsync throws an InvalidOperationException with this message when DocumentAccessPermissions of the PDFSettings is not set, and in its overload without PDFSettings. Set the property, for example to DocumentAccessPermissions.AllowAll, or use ConvertAsync with ReturnFormat.PDF or ReturnFormat.PDFA when you need no PDF settings.
A Session Is Not Found
GetSession and the indexer throw a KeyNotFoundException, which ends the request with an error 500. Use TryGetSession and answer 404, as the controller above does, and check that the request reaches the server that holds the session.
Next Steps
- Custom Signing Endpoints: receive the documents signed in the Document Viewer in a plugin.
- Creating a Plugin: accept only requests with a valid access token.
- TXTextControl.DocumentServices.SamplePlugin: the official sample plugin, with controllers for both services.
- Announcing TX Text Control DS Server 5.0: the release article that introduced the services.