13 KiB
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