DS Server

Creating a Plugin

Topic

A plugin starts as an ordinary .NET class library. You add one NuGet package, implement the IPlugin interface, copy the compiled assembly into the Plugins folder of DS Server and restart the server. Follow the steps below to build MyFirstPlugin, a plugin with its own endpoint at /plugin/hello, a setting read from the configuration of DS Server and a middleware that logs each request to the endpoint.

The steps target DS Server 5.0 and version 5.0.1 of the plugin package. Plugins explains what a plugin can do and how DS Server loads it.

Prerequisites

  • .NET 10 SDK. DS Server 5.0 runs on .NET 10, and the plugin targets net10.0, as the official sample plugin does.
  • A DS Server 5.0 installation that you can stop and start, with write access to its folder. Installation Guide describes the ways to install it.
  • A terminal for the .NET CLI and cURL. Visual Studio works as well: create a Class Library project for .NET 10 and add the package with the NuGet Package Manager.

Create the Project

Create a class library and remove the class the template adds:

dotnet new classlib -n MyFirstPlugin
cd MyFirstPlugin
rm Class1.cs

Install the NuGet Package

The interfaces of a plugin are in the package TXTextControl.DocumentServices.Plugin.Abstractions on nuget.org:

dotnet add package TXTextControl.DocumentServices.Plugin.Abstractions

The project file then references the package:

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

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="TXTextControl.DocumentServices.Plugin.Abstractions" Version="5.0.1" />
  </ItemGroup>

</Project>

The package brings the ASP.NET Core shared framework into the project, so controllers, middleware and dependency injection are available without another reference. The official sample plugin also adds <FrameworkReference Include="Microsoft.AspNetCore.App" /> explicitly, which does no harm.

Use version 5.0.0 or later of the package with DS Server 5.0; Versions lists which package fits which DS Server version. Version 4.1.0 lacks the constant PluginContext.AuthenticationScheme, which Protect the Endpoint With OAuth uses, and the services of Plugin Services.

Write the Code

The plugin consists of three files: a class that holds a setting, a controller and the plugin class.

The Setting

GreetingState.cs holds the greeting the endpoint returns:

namespace MyFirstPlugin;

public class GreetingState
{
    public string Greeting { get; set; } = "Hello (default)";
}

The Controller

HelloController.cs is a standard ASP.NET Core API controller. DS Server adds the controllers of a plugin to its own routes, so the endpoint answers at /plugin/hello on the address of DS Server:

using Microsoft.AspNetCore.Mvc;

namespace MyFirstPlugin;

[ApiController]
[Route("plugin/[controller]")]
public class HelloController : ControllerBase
{
    private readonly GreetingState m_state;

    public HelloController(GreetingState state)
    {
        m_state = state;
    }

    [HttpGet]
    public string Get() => m_state.Greeting;
}

The controller receives GreetingState through its constructor. The plugin class registers it in the next step.

The Plugin Class

HelloPlugin.cs implements IPlugin. DS Server finds this class in the assembly and calls its members while it starts:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TXTextControl.DocumentServices.Plugin.Abstractions;

namespace MyFirstPlugin;

public class HelloPlugin : IPlugin
{
    public string Name => "MyFirstPlugin";
    public string Description => "Adds a /plugin/hello endpoint and logs its requests.";
    public string Version => "1.0.0";

    public void ConfigureServices(IServiceCollection services, PluginContext context)
    {
        // Read the greeting from the appsettings.json file of DS Server
        string greeting = context.Configuration["MyFirstPlugin:Greeting"] ?? "Hello (default)";
        services.AddSingleton(new GreetingState { Greeting = greeting });
    }

    public void ConfigureMiddleware(WebApplication app, PluginContext context)
    {
        // Log every request to the plugin's endpoint
        app.Use(async (httpContext, next) =>
        {
            if (httpContext.Request.Path.StartsWithSegments("/plugin/hello"))
            {
                ILogger<HelloPlugin> logger =
                    httpContext.RequestServices.GetRequiredService<ILogger<HelloPlugin>>();
                logger.LogInformation("MyFirstPlugin intercepted a request to /plugin/hello");
            }

            await next();
        });
    }

    public void OnStart(IServiceProvider services, PluginContext context)
    {
        ILogger<HelloPlugin>? logger = services.GetService<ILogger<HelloPlugin>>();
        GreetingState? state = services.GetService<GreetingState>();

        logger?.LogInformation("MyFirstPlugin started on DS Server {Version}. Greeting: {Greeting}",
            context.DsServerVersion, state?.Greeting);
    }

    public void OnStop()
    {
        // Release the resources the plugin holds
    }
}

What the Code Does

The members of IPlugin are called in this order. Only Name, Description and Version are required: the interface has empty default implementations of the other members, so a plugin implements only the ones it needs.

MemberWhen DS Server uses itWhat the plugin does there
Name, Description, VersionAt startup and on the Plugins page of the admin portalIdentify the plugin
UIBasePathOn the Plugins pageOptional: the address of a web page of the plugin, see Add a Page to the Admin Portal
ConfigureServices(IServiceCollection, PluginContext)Before the DS Server application is builtRegister services in the dependency injection container
ConfigureMiddleware(WebApplication, PluginContext)After the application is builtAdd middleware and endpoints to the request pipeline
OnStart(IServiceProvider, PluginContext)When the plugin is loaded, right before DS Server starts serving requestsInitialize resources, with access to the registered services
OnStop()When the plugin is unloadedRelease resources

PluginContext gives the plugin three properties: DsServerVersion, the version string of DS Server, such as 5.0.2.0; BaseDirectory, the installation folder of DS Server; and Configuration, the configuration of the DS Server instance, which includes its appsettings.json file. Since version 5.0.0 of the package, it also has the constant AuthenticationScheme.

In the code above:

  • ConfigureServices reads the key MyFirstPlugin:Greeting from context.Configuration and registers a GreetingState as a singleton, which the controller receives.
  • ConfigureMiddleware adds a middleware with app.Use. It writes a log entry for every request whose path starts with /plugin/hello and passes the request on with next().
  • OnStart writes the version of DS Server and the greeting to the log.

The documentation of the package lists every type: Plugin.Abstractions.

Configure the Plugin

A plugin reads its settings from the appsettings.json file in the DS Server folder. Add a section for MyFirstPlugin beside the existing ones:

{
  "Logging": { },
  "AllowedHosts": "*",
  "LiteDbOptions": {
    "DatabaseFileName": "Admin.db"
  },
  "MyFirstPlugin": {
    "Greeting": "Hello from MyFirstPlugin!"
  }
}

Keep the content of the existing sections; the example shortens Logging. Without the section, the endpoint returns the default greeting.

Build and Deploy the Plugin

  1. Build the project:

    dotnet build
    

    The assembly is written to bin/Debug/net10.0/MyFirstPlugin.dll.

  2. Stop DS Server.

  3. Create a subfolder for the plugin in the Plugins folder of DS Server and copy the assembly into it:

    Plugins/
    ├── README.md
    └── MyFirstPlugin/
        └── MyFirstPlugin.dll
    

    The name of the subfolder is free. An assembly directly in Plugins is not loaded.

  4. Start DS Server.

A plugin that references other NuGet packages needs their assemblies in the same subfolder. dotnet publish -c Release -o publish collects the plugin and its dependencies in the publish folder; copy its content into the subfolder. DS Server loads plugins only when it starts, so repeat the restart after every update of the assembly.

Test the Plugin

When DS Server starts, its console output lists every plugin it loads:

Loading plugins from '/opt/dsserver/Plugins'...
Loaded plugin: MyFirstPlugin (v1.0.0)

The entry from OnStart is in the log file of the day, in the logs folder that DS Server creates in its working directory:

[2026-09-29 07:44:57] [Information] MyFirstPlugin started on DS Server 5.0.2.0. Greeting: Hello from MyFirstPlugin!

Call the endpoint. Replace the address with the one of your DS Server:

curl http://localhost:5000/plugin/hello
Hello from MyFirstPlugin!

The middleware adds a line to the log for each request:

[2026-09-29 07:45:06] [Information] MyFirstPlugin intercepted a request to /plugin/hello

In the admin portal, the Plugins page lists the plugin with its name, version and description and shows it as enabled:

The Plugins page of the admin portal lists MyFirstPlugin with its version and description

Protect the Endpoint With OAuth

The endpoints of a plugin are open: DS Server answers /plugin/hello without an access token, even when OAuth is enabled for the built-in services. To accept only requests with a valid access token of a security profile, add the Authorize attribute with the authentication scheme of DS Server. The constant PluginContext.AuthenticationScheme of the package holds its name, TXTextControl:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TXTextControl.DocumentServices.Plugin.Abstractions;

namespace MyFirstPlugin;

[ApiController]
[Route("plugin/[controller]")]
[Authorize(AuthenticationSchemes = PluginContext.AuthenticationScheme)]
public class SecretController : ControllerBase
{
    [HttpGet]
    public string Get() => "Only for callers with a valid access token";
}

The scheme checks the access token as it does for the built-in services. It accepts the token in an Authorization: Bearer header, in the access_token query parameter or in an access token cookie:

Request to /plugin/secretOAuth enabledOAuth disabled
Without a token401 Unauthorized200 OK
With an invalid token401 Unauthorized200 OK
With a valid access token200 OK200 OK

Name the scheme in the attribute: [Authorize] without a scheme ends in an error 500, because DS Server defines no default scheme for it. A client obtains the token as described in OAuth Grant Types, and Enable OAuth describes the switch in the admin portal.

Add a Page to the Admin Portal

A plugin with a web interface returns its address in UIBasePath. The Plugins page then shows the plugin name as a link that opens this address in a new tab. Add the property to HelloPlugin and map a page to it at the end of ConfigureMiddleware:

// The address the Plugins page of the admin portal links the plugin name to
public string UIBasePath => "/plugin-ui/my-first-plugin";

public void ConfigureMiddleware(WebApplication app, PluginContext context)
{
    // The logging middleware from above goes here

    // Serve a page at UIBasePath
    GreetingState state = app.Services.GetRequiredService<GreetingState>();
    app.MapGet(UIBasePath, () => Results.Content(
        $"<html><body><h3>MyFirstPlugin</h3><p>Greeting: {state.Greeting}</p></body></html>",
        "text/html"));
}

The page is served without the admin password of the portal. Protect it yourself if it shows or changes settings.

Troubleshooting

The Type WebApplication Is Not Found

error CS0246: The type or namespace name 'WebApplication' could not be found

The implicit using directives of a class library do not include ASP.NET Core. Add using Microsoft.AspNetCore.Builder; to the file of the plugin class. The first plugin in the README of the package and in the blog article of version 4.1.0 lacks this directive.

DS Server Loads No Plugin

No plugins found in the plugin directory. No plugins will be loaded.
  1. Check that the assembly is in a subfolder of Plugins, not in Plugins itself.
  2. Check that the subfolder contains an assembly with a public class that implements IPlugin.
  3. Restart DS Server after copying the files.

When DS Server finds assemblies but none of them holds a plugin it can load, it reports instead:

Warning: No valid plugins found in the plugin directory. Make sure that the plugin assemblies reference the correct version of the TXTextControl.DocumentServices.Plugin.Abstractions.dll and that the plugins implement the IPlugin interface correctly.

Check the package version against Versions, and check the class as in step 2. A plugin that fails while it is loaded is reported in a line that starts with Failed to load plugin and ends with the error.

A Request Ends in an Error 500 After Adding Authorize

The attribute has no authentication scheme. Write [Authorize(AuthenticationSchemes = PluginContext.AuthenticationScheme)], or its value "TXTextControl", as shown in Protect the Endpoint With OAuth.

Next Steps