TX Text Control .NET Server

Multiple Processes

Topic

An application with the Document Editor is more than one process. Next to your ASP.NET Core application runs the backend TXTextControl.Web.Server.Core, which synchronizes the editing sessions, and the backend starts a worker process for every editor that connects. In containers, you decide whether the backend shares the container of your application or runs in a container of its own.

Both layouts are described below: what runs where, how the application and the backend find each other and what happens when one of them stops. Applications without the Document Editor, which only process documents or use the Document Viewer, run as a single process and need none of this.

The Processes of an Application

dotnet MyApp.dll                                  your application, the entry point of the container
  |
  +-- dotnet TXTextControl.Web.Server.Core.dll    the backend, started by DocumentEditorWorkerManager
        |
        +-- dotnet TXTextControl.Web.Server.Core.Process.dll   one worker process for each open editor
  • Your application hosts the WebSocket middleware, which passes the traffic of each editor over TCP to the backend. ServerTextControl, MailMerge and the Document Viewer work inside this process and start no other processes.
  • The backend listens on TCP port 4283 by default and starts the worker processes. It communicates with them over pipes, which need no ports.
  • A worker process holds the document of one editing session in memory until your code saves it. It ends when the editor closes or when a lost connection isn't restored in time.

The hosted service DocumentEditorWorkerManager starts the backend when your application starts and stops the backend and all worker processes when the application shuts down. It doesn't restart a backend that ends on its own: the application keeps running, and editors can't connect until the application restarts.

Choose Where the Backend Runs

In the application containerIn its own container
SetupAddHostedService<DocumentEditorWorkerManager>(), nothing elseA second image, and the address of the backend in the application
ScalingApplication and backend scale togetherThe backend scales separately from the application
When a container stopsApplication, backend and editing sessions stop togetherThe sessions in a stopped backend container end; the application keeps running
Use it forMost applicationsDeployments that separate real-time editing from the application, as Text Control recommends for many concurrent users

Scaling Beyond One Instance describes Text Control's recommendation for larger deployments.

Run the Backend in the Application Container

The image from Dockerfile already runs the backend: the application starts it inside the container, and the backend's messages appear in the container output with the prefix [TXTextControl.Web.Server.Core].

Without configuration, the backend listens on all network interfaces of the container, and it accepts TCP connections from any client. Other containers in the same network could connect to port 4283. Bind the backend to the loopback address of the container instead, with the address and port in the DocumentEditorWorker configuration section and the same values in Program.cs, as Prepare the Application shows. In a container, environment variables set the section, with two underscores in place of the colon:

docker run --rm -p 8080:8080 \
  -e DocumentEditorWorker__ListenAddress=127.0.0.1 \
  -e DocumentEditorWorker__Port=4283 \
  myapp

The application log then shows "Listen address "127.0.0.1" provided via options." and the backend "Starting TCP listener on port 4283 for IPv4.".

Run the Backend in Its Own Container

A separate backend needs its own image, a listen address that other containers can reach and an application that connects to it by name.

Create the Backend Project

The backend image contains the backend files, the Core engine it uses and their native libraries and fonts. A class library that references the two packages publishes all of them. Create Backend/Backend.csproj next to the folder of your application:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="TXTextControl.TextControl.Core.SDK" Version="34.0.5" />
    <PackageReference Include="TXTextControl.Web.DocumentEditor.Backend" Version="34.5.0" />
  </ItemGroup>

</Project>

The project needs no code. Use the same package versions as your application.

Build the Backend Image

Create Backend/Dockerfile. It follows the Dockerfile of the application and starts the backend instead:

# syntax=docker/dockerfile:1

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

COPY Backend/Backend.csproj Backend/
RUN --mount=type=secret,id=nugetconfig \
    dotnet restore Backend/Backend.csproj -r linux-x64 --configfile /run/secrets/nugetconfig

COPY Backend/ Backend/
RUN dotnet publish Backend/Backend.csproj -c Release -r linux-x64 --self-contained false \
    --no-restore -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
EXPOSE 4283
ENTRYPOINT ["dotnet", "TXTextControl.Web.Server.Core.dll", "--listen-address", "0.0.0.0", "--port", "4283"]

--listen-address 0.0.0.0 makes the backend reachable from the application container, and --port sets its port. Build both images from the folder that contains nuget.config, the Dockerfile of the application and the two project folders:

docker buildx build --secret id=nugetconfig,src=nuget.config -t myapp .
docker buildx build -f Backend/Dockerfile --secret id=nugetconfig,src=nuget.config -t mybackend .

Connect the Application to the Backend

The application doesn't start a backend of its own. Remove AddHostedService<DocumentEditorWorkerManager>() from Program.cs and pass the name and port of the backend to the middleware:

using TXTextControl.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

app.UseRouting();

app.UseWebSockets();

// Connect the editors to the backend container
app.UseTXWebSocketMiddleware("backend", 4283);

app.UseAuthorization();

app.MapStaticAssets();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

app.Run();

The code works as follows:

  • UseTXWebSocketMiddleware("backend", 4283) resolves the host name backend to an address once, when the application starts, and connects every editor to this address and port.
  • If the name can't be resolved at that moment, the application doesn't start. The log shows a SocketException thrown in the constructor of TXTextControl.Web.WebSocketMiddleware.
  • If the backend isn't running when an editor connects, the editor doesn't load, and the browser console shows "WebSocketHandler.DoTalkingAsync(): Could not connect to TextControl TCP server" with the address and port.

Start Both With Docker Compose

A compose.yaml file starts both containers in one network, in which each service is reachable by its name:

services:
  app:
    image: myapp
    ports:
      - "8080:8080"
    depends_on:
      - backend

  backend:
    image: mybackend
    expose:
      - "4283"
    restart: unless-stopped

The file works as follows:

  • backend is the service name that the application passes to UseTXWebSocketMiddleware.
  • expose makes port 4283 reachable for the other services without publishing it on the host. Don't add a ports entry for the backend.
  • depends_on starts the backend before the application, so its name exists when the application resolves it.
  • restart: unless-stopped restarts the backend container when the backend process ends. Editing sessions of the ended process are lost.

A backend container that is recreated can get a new address. Because the application resolved the name at startup, restart the application container as well.

When an editor connects, the output of the backend container shows "Starting web editor worker process with connection ID" followed by the ID of the connection.

Next Steps