Files
git.stella-ops.org/docs/router/transports/development.md
StellaOps Bot e6c47c8f50 save progress
2025-12-28 23:49:56 +02:00

13 KiB

Transport Plugin Development Guide

This guide explains how to create custom router transport plugins for StellaOps.

Overview

Router transport plugins implement the IRouterTransportPlugin interface to provide custom communication protocols. The plugin system enables:

  • Runtime loading of transport implementations
  • Isolation from the Gateway codebase
  • Hot-swappable transports (restart required)
  • Third-party transport extensions

Prerequisites

  • .NET 10 SDK
  • Understanding of async socket programming
  • Familiarity with dependency injection

Creating a Transport Plugin

Step 1: Create Project

mkdir MyCompany.Router.Transport.Custom
cd MyCompany.Router.Transport.Custom
dotnet new classlib -f net10.0
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Microsoft.Extensions.Options

Add project reference to Router.Common:

<ItemGroup>
  <ProjectReference Include="path/to/StellaOps.Router.Common/StellaOps.Router.Common.csproj" />
</ItemGroup>

Step 2: Create Options Class

namespace MyCompany.Router.Transport.Custom;

/// <summary>
/// Configuration options for the custom transport.
/// </summary>
public sealed class CustomTransportOptions
{
    /// <summary>
    /// Host address to bind/connect to.
    /// </summary>
    public string Host { get; set; } = "0.0.0.0";

    /// <summary>
    /// Port number.
    /// </summary>
    public int Port { get; set; } = 5200;

    /// <summary>
    /// Connection timeout.
    /// </summary>
    public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(30);

    /// <summary>
    /// Maximum concurrent connections.
    /// </summary>
    public int MaxConnections { get; set; } = 1000;
}

Step 3: Implement Transport Server

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;

namespace MyCompany.Router.Transport.Custom;

public sealed class CustomTransportServer : ITransportServer
{
    private readonly CustomTransportOptions _options;
    private readonly ILogger<CustomTransportServer> _logger;

    public CustomTransportServer(
        IOptions<CustomTransportOptions> options,
        ILogger<CustomTransportServer> logger)
    {
        _options = options.Value;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Starting custom transport server on {Host}:{Port}",
            _options.Host, _options.Port);

        // Initialize your transport server (socket, listener, etc.)
        // ...
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Stopping custom transport server");

        // Graceful shutdown
        // ...
    }

    public async Task<ITransportConnection> AcceptAsync(CancellationToken cancellationToken)
    {
        // Accept incoming connection
        // Return ITransportConnection implementation
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        // Cleanup resources
    }
}

Step 4: Implement Transport Client

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;

namespace MyCompany.Router.Transport.Custom;

public sealed class CustomTransportClient : ITransportClient, IMicroserviceTransport
{
    private readonly CustomTransportOptions _options;
    private readonly ILogger<CustomTransportClient> _logger;

    public CustomTransportClient(
        IOptions<CustomTransportOptions> options,
        ILogger<CustomTransportClient> logger)
    {
        _options = options.Value;
        _logger = logger;
    }

    public async Task<ITransportConnection> ConnectAsync(
        string host,
        int port,
        CancellationToken cancellationToken)
    {
        _logger.LogDebug("Connecting to {Host}:{Port}", host, port);

        // Establish connection
        // Return ITransportConnection implementation
        throw new NotImplementedException();
    }

    public async Task DisconnectAsync(CancellationToken cancellationToken)
    {
        // Disconnect from server
    }

    public void Dispose()
    {
        // Cleanup resources
    }
}

Step 5: Implement Connection

using StellaOps.Router.Common.Abstractions;

namespace MyCompany.Router.Transport.Custom;

public sealed class CustomTransportConnection : ITransportConnection
{
    public string ConnectionId { get; } = Guid.NewGuid().ToString("N");
    public bool IsConnected { get; private set; }
    public EndPoint? RemoteEndPoint { get; private set; }

    public async Task<int> SendAsync(
        ReadOnlyMemory<byte> data,
        CancellationToken cancellationToken)
    {
        // Send data over transport
        throw new NotImplementedException();
    }

    public async Task<int> ReceiveAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken)
    {
        // Receive data from transport
        throw new NotImplementedException();
    }

    public async Task CloseAsync(CancellationToken cancellationToken)
    {
        IsConnected = false;
        // Close connection
    }

    public void Dispose()
    {
        // Cleanup
    }
}

Step 6: Implement Plugin

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Plugins;

namespace MyCompany.Router.Transport.Custom;

/// <summary>
/// Plugin implementation for custom transport.
/// </summary>
public sealed class CustomTransportPlugin : IRouterTransportPlugin
{
    /// <inheritdoc />
    public string TransportName => "custom";

    /// <inheritdoc />
    public string DisplayName => "Custom Transport";

    /// <inheritdoc />
    public bool IsAvailable(IServiceProvider services)
    {
        // Check if required dependencies are available
        // Return false if transport cannot be used in current environment
        return true;
    }

    /// <inheritdoc />
    public void Register(RouterTransportRegistrationContext context)
    {
        var services = context.Services;
        var configuration = context.Configuration;

        // Bind configuration
        var configSection = context.ConfigurationSection is not null
            ? configuration.GetSection(context.ConfigurationSection)
            : configuration.GetSection("Router:Transport:Custom");

        services.AddOptions<CustomTransportOptions>();
        if (configSection.GetChildren().Any())
        {
            services.Configure<CustomTransportOptions>(options =>
            {
                configSection.Bind(options);
            });
        }

        // Register server if requested
        if (context.Mode.HasFlag(RouterTransportMode.Server))
        {
            services.AddSingleton<CustomTransportServer>();
            services.AddSingleton<ITransportServer>(sp =>
                sp.GetRequiredService<CustomTransportServer>());
        }

        // Register client if requested
        if (context.Mode.HasFlag(RouterTransportMode.Client))
        {
            services.AddSingleton<CustomTransportClient>();
            services.AddSingleton<ITransportClient>(sp =>
                sp.GetRequiredService<CustomTransportClient>());
            services.AddSingleton<IMicroserviceTransport>(sp =>
                sp.GetRequiredService<CustomTransportClient>());
        }
    }
}

Building and Packaging

Build Configuration

Add to your .csproj:

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
  <RootNamespace>MyCompany.Router.Transport.Custom</RootNamespace>

  <!-- Plugin assembly attributes -->
  <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>

<!-- Output to plugins directory -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <OutputPath>$(SolutionDir)plugins\router\transports\</OutputPath>
  <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>

Build Commands

# Debug build
dotnet build

# Release build to plugins directory
dotnet build -c Release

# Publish with all dependencies
dotnet publish -c Release -o ./publish

Configuration Schema

Create plugin.json manifest:

{
  "schemaVersion": "2.0",
  "id": "mycompany.router.transport.custom",
  "name": "Custom Transport",
  "version": "1.0.0",
  "assembly": {
    "path": "MyCompany.Router.Transport.Custom.dll",
    "entryType": "MyCompany.Router.Transport.Custom.CustomTransportPlugin"
  },
  "capabilities": ["server", "client", "streaming"],
  "platforms": ["linux-x64", "win-x64", "osx-arm64"],
  "enabled": true,
  "priority": 100
}

Create config.yaml for runtime configuration:

id: mycompany.router.transport.custom
name: Custom Transport
enabled: true
config:
  host: "0.0.0.0"
  port: 5200
  maxConnections: 1000
  connectTimeout: "00:00:30"

Testing

Unit Tests

public class CustomTransportPluginTests
{
    [Fact]
    public void TransportName_ReturnsCustom()
    {
        var plugin = new CustomTransportPlugin();
        Assert.Equal("custom", plugin.TransportName);
    }

    [Fact]
    public void Register_AddsServerServices()
    {
        var plugin = new CustomTransportPlugin();
        var services = new ServiceCollection();
        var config = new ConfigurationBuilder().Build();

        var context = new RouterTransportRegistrationContext(
            services, config, RouterTransportMode.Server);

        plugin.Register(context);

        var provider = services.BuildServiceProvider();
        Assert.NotNull(provider.GetService<ITransportServer>());
    }
}

Integration Tests

public class CustomTransportIntegrationTests
{
    [Fact]
    public async Task Server_AcceptsConnections()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.Configure<CustomTransportOptions>(opts =>
        {
            opts.Port = 15200;  // Test port
        });
        services.AddSingleton<CustomTransportServer>();

        var provider = services.BuildServiceProvider();
        var server = provider.GetRequiredService<CustomTransportServer>();

        await server.StartAsync(CancellationToken.None);

        // Connect client and verify
        // ...

        await server.StopAsync(CancellationToken.None);
    }
}

Best Practices

Error Handling

public async Task<ITransportConnection> ConnectAsync(
    string host,
    int port,
    CancellationToken cancellationToken)
{
    try
    {
        // Attempt connection
        return await ConnectInternalAsync(host, port, cancellationToken);
    }
    catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
    {
        _logger.LogWarning("Connection refused to {Host}:{Port}", host, port);
        throw new TransportConnectionException($"Connection refused to {host}:{port}", ex);
    }
    catch (OperationCanceledException)
    {
        _logger.LogDebug("Connection attempt cancelled");
        throw;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to connect to {Host}:{Port}", host, port);
        throw new TransportConnectionException($"Failed to connect to {host}:{port}", ex);
    }
}

Resource Management

public sealed class CustomTransportServer : ITransportServer, IAsyncDisposable
{
    private readonly SemaphoreSlim _connectionLock = new(1, 1);
    private readonly List<ITransportConnection> _connections = [];
    private bool _disposed;

    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;

        await _connectionLock.WaitAsync();
        try
        {
            foreach (var conn in _connections)
            {
                await conn.CloseAsync(CancellationToken.None);
                conn.Dispose();
            }
            _connections.Clear();
        }
        finally
        {
            _connectionLock.Release();
            _connectionLock.Dispose();
        }
    }
}

Logging

// Use structured logging
_logger.LogInformation(
    "Connection established {ConnectionId} from {RemoteEndPoint}",
    connection.ConnectionId,
    connection.RemoteEndPoint);

// Include correlation IDs
using (_logger.BeginScope(new Dictionary<string, object>
{
    ["ConnectionId"] = connectionId,
    ["TraceId"] = Activity.Current?.TraceId.ToString() ?? "N/A"
}))
{
    await ProcessConnectionAsync(connection, cancellationToken);
}

Deployment

Copy to Plugins Directory

cp ./publish/*.dll /opt/stellaops/plugins/router/transports/

Verify Plugin Loading

# Check logs for plugin discovery
grep "Loaded router transport plugin" /var/log/stellaops/gateway.log

Configuration

# router.yaml
Router:
  Transport:
    Type: custom
    Custom:
      Host: "0.0.0.0"
      Port: 5200

See Also