DS Server

Plugin Services

Topic

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, with services.GetService<IDocumentProcessingService>() on the IServiceProvider that DS Server passes.

The interfaces are in the package TXTextControl.DocumentServices.Plugin.Abstractions 5.0.0 and later, in these namespaces:

NamespaceTypes
TXTextControl.DocumentServices.DocumentProcessing.AbstractionsIDocumentProcessingService and its sub-APIs IPdfApi, IAcroFormsApi and IBarcodesApi
TXTextControl.DocumentServices.DocumentProcessing.ModelsThe data of the processing service, such as MergeBody, DocumentInfo, PDFSettings, BarcodeSettings and the enumeration ReturnFormat
TXTextControl.DocumentServices.DocumentProcessingDocumentProcessingException, thrown when an error occurs during document processing
TXTextControl.DocumentServices.DocumentEditor.AbstractionsIDocumentEditorSessionService, IDocumentEditorSession and the document objects of a session, such as ISelection and IApplicationField
TXTextControl.DocumentServices.DocumentEditor.OptionsLoadOptions and SaveOptions
TXTextControl.DocumentServices.DocumentEditor.EnumsDocumentFormat and the other enumerations of a session
TXTextControl.DocumentServices.Plugin.Abstractions.DocumentEditor.ModelsSavedDocument, 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 propertyWhat it does
MergeAsyncMerges 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
ConvertAsyncConverts a document into a ReturnFormat: PDF, PDFA, DOC, DOCX, RTF, TX, TXT or HTML. An overload flattens form fields
AppendAsyncCombines several documents, given as an AppendBody
FindAndReplaceAsyncReplaces text in a document, given as a FindAndReplaceBody
GetThumbnailsAsyncCreates images of the pages, with a zoom factor between 1 and 400 and an optional page range
GetDocumentInfoAsyncReturns a DocumentInfo with the merge fields, merge blocks and document properties of a document
PDFThe IPdfApi: CreateAsync creates a PDF or PDF/A document (PDFFormat) with PDFSettings, GetEmbeddedFilesAsync and GetMetaDataAsync read a PDF
PDF.AcroFormsThe IAcroFormsApi: GetFieldsAsync returns the form fields of a PDF
BarcodesThe 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;

[ApiController]
[Route("plugin/reports")]
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
    [HttpPost("merge")]
    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
    [HttpPost("fields")]
    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 IDocumentProcessingService from DS Server.
  • Merge decodes the Base64-encoded template and passes the JSON array of the request unchanged as MergeDataJson. The JSON must be an array of objects of the same type.
  • MergeAsync with ReturnFormat.PDF and true for append merges every record and appends the results to one document, which is the first element of the returned array.
  • Fields calls GetDocumentInfoAsync and returns its MergeFields, one entry per merge field with its name and text.
  • HttpContext.RequestAborted cancels 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 -X POST http://localhost:5000/plugin/reports/merge \
  -H "Content-Type: application/json" \
  --data @merge-request.json \
  --output 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:

MemberWhat 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:

MemberWhat it does
ConnectionIdThe 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 moreLoad data for template design: the merge fields and blocks the user can insert
SelectionThe current selection, with methods such as GetTextAsync, SetTextAsync and ParagraphFormat.SetBackColorAsync
ApplicationFields, FormFields, Paragraphs, TextFrames, HeadersAndFootersThe fields and other elements of the document
SetEditModeAsync, SetUserNamesAsync, SetControlCharsAsyncSettings 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;

[ApiController]
[Route("plugin/editor/{connectionId}")]
public class EditorSessionsController : ControllerBase
{
    private readonly IDocumentEditorSessionService m_sessions;

    public EditorSessionsController(IDocumentEditorSessionService sessions)
    {
        m_sessions = sessions;
    }

    // Replaces the document in the editor with HTML
    [HttpPost("load")]
    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
    [HttpGet("selection")]
    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
    [HttpGet("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.
  • TryGetSession finds the session. The controller answers 404 when the user has closed the editor or the ID belongs to another server.
  • LoadOptions.FromDocumentFormat with DocumentFormat.HTML and a string creates the options for text formats; for DOCX or PDF, pass the bytes of the document instead.
  • LoadAsync replaces the document the user sees.
  • Selection.GetTextAsync returns the text the user has selected.
  • SaveAsync with new SaveOptions(DocumentFormat.PDF) returns a SavedDocument whose Content holds 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