535 lines
13 KiB
Markdown
535 lines
13 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<ProjectReference Include="path/to/StellaOps.Router.Common/StellaOps.Router.Common.csproj" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
### Step 2: Create Options Class
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```xml
|
|
<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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```yaml
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
```bash
|
|
cp ./publish/*.dll /opt/stellaops/plugins/router/transports/
|
|
```
|
|
|
|
### Verify Plugin Loading
|
|
|
|
```bash
|
|
# Check logs for plugin discovery
|
|
grep "Loaded router transport plugin" /var/log/stellaops/gateway.log
|
|
```
|
|
|
|
### Configuration
|
|
|
|
```yaml
|
|
# router.yaml
|
|
Router:
|
|
Transport:
|
|
Type: custom
|
|
Custom:
|
|
Host: "0.0.0.0"
|
|
Port: 5200
|
|
```
|
|
|
|
## See Also
|
|
|
|
- [Transport Overview](./README.md)
|
|
- [Plugin SDK Guide](../../10_PLUGIN_SDK_GUIDE.md)
|
|
- [IRouterTransportPlugin API](../../../src/Router/__Libraries/StellaOps.Router.Common/Plugins/IRouterTransportPlugin.cs)
|