# Plugin Services

> Merge, convert and inspect documents in a DS Server plugin with IDocumentProcessingService, and load or save editor sessions with IDocumentEditorSessionService.

- **Product:** DS Server
- **Section:** Documentation
- **Kind:** Topic
- **Path:** DS Server > Plugins > Plugin Services
- **4993 words**
- **Web URL:** https://docs.textcontrol.com/ds-server/plugins/services/
- **LLMs URL:** https://docs.textcontrol.com/ds-server/plugins/services/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/ds-server/plugins/services/llms-full.txt

---

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](https://docs.textcontrol.com/ds-server/plugins/creating/llms-full.txt).

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

| 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](https://docs.textcontrol.com/ds-server/reference/dotnet-api/txtextcontrol-documentservices-plugin-abstractions/llms-full.txt).

### 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](https://docs.textcontrol.com/ds-server/document-processing/llms-full.txt).

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

```csharp
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:

```json
{
  "template": "<Base64-encoded template>",
  "data": [
    { "company_name": "Text Control", "total": "1,250.00" },
    { "company_name": "Contoso", "total": "990.00" }
  ]
}
```

```bash
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:

| 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`](https://docs.textcontrol.com/ds-server/reference/javascript/txtextcontrol/connectionid-property/llms-full.txt) property of the `TXTextControl` object. Read it once the editor is ready, with [`addEventListener`](https://docs.textcontrol.com/ds-server/reference/javascript/txtextcontrol/addeventlistener-method/llms-full.txt), and send it to the code that calls the plugin:

```javascript
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](https://docs.textcontrol.com/ds-server/installation/load-balancing/llms-full.txt).

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

```csharp
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

- [Custom Signing Endpoints](https://docs.textcontrol.com/ds-server/plugins/signing/llms-full.txt): receive the documents signed in the Document Viewer in a plugin.
- [Creating a Plugin](https://docs.textcontrol.com/ds-server/plugins/creating/#protect-the-endpoint-with-oauth): accept only requests with a valid access token.
- [TXTextControl.DocumentServices.SamplePlugin](https://github.com/TextControl/TXTextControl.DocumentServices.SamplePlugin): the official sample plugin, with controllers for both services.
- [Announcing TX Text Control DS Server 5.0](https://www.textcontrol.com/blog/2026/03/12/announcing-tx-text-control-ds-server-5-0/): the release article that introduced the services.

---

## Nearby

- **Previous:** [Creating a Plugin](https://docs.textcontrol.com/ds-server/plugins/creating/llms-full.txt)
- **Next:** [Custom Signing Endpoints](https://docs.textcontrol.com/ds-server/plugins/signing/llms-full.txt)
