Remote Commands
A TCP listener that receives length-prefixed JSON payloads and dispatches each to a typed command handler, for an internal operational channel or automation hook.
A command inherits RemoteCommand<T> and carries RemoteCommandAttribute, with T the message record the payload binds to. Its reply is written through ICommandResponse, so a command never touches the socket and the client reads one frame per request.
Adopting it is one registration, one assembly scan, and a hosted service that runs the listener. The listener answers a request it cannot serve with a refused RemoteCommandResponse rather than throwing, so the failures a caller has to handle live on RemoteCommandClient instead.
Categories
- Configuration — the
RemoteServersection the listener binds and validates at startup. - Exceptions — what the client throws when a send fails, one type per reason.
- Extensions — startup extension methods for registering the listener and command handlers.
- Attributes — metadata used to name and describe remote commands.
- Services — the listener that answers commands and the client that sends them.
- Types — the base type application commands inherit, and the response handle one writes its reply through.
- Records — the request and response frames on the wire.
Quick Example
csharp
using Microsoft.Extensions.Hosting;
using AlmightyShogun.RemoteCommands;
using Microsoft.Extensions.DependencyInjection;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddRemoteCommands(builder.Configuration)
.RegisterRemoteCommands()
.AddHostedService<RemoteCommandWorker>();
await builder.Build().RunAsync();csharp
using Microsoft.Extensions.Hosting;
using AlmightyShogun.RemoteCommands;
public sealed class RemoteCommandWorker(
IRemoteCommandHandler commandHandler
) : BackgroundService
{
protected override Task ExecuteAsync(
CancellationToken cancellationToken
) => commandHandler.StartAsync(cancellationToken);
}csharp
using AlmightyShogun.RemoteCommands;
[RemoteCommand("drain", "Stops accepting work and reports what is still in flight.")]
public sealed class DrainCommand(WorkQueue queue) : RemoteCommand<DrainMessage>
{
public override async Task HandleCommandAsync(
DrainMessage message,
ICommandResponse response,
CancellationToken cancellationToken
)
{
int remaining = await queue.DrainAsync(message.Reason, cancellationToken);
await response.WriteAsync(
new DrainResponse(remaining, DateTimeOffset.UtcNow),
cancellationToken
);
}
}csharp
public sealed record DrainMessage
{
public required string Reason { get; init; }
}csharp
public sealed record DrainResponse(
int Remaining,
DateTimeOffset DrainedAt
);