Snippet
Host a loopback helper API
A complete ASP.NET Core loopback host and router for a native helper's pairing and capture contract.
AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy
What it does#
Hosts a small helper API on 127.0.0.1 only. The router exposes health, pairing, capture start, capture stop, status, disconnect, and revoke operations. The backend implementation is injected, so this file stays a transport adapter.
You need#
- .NET SDK 10
- A project with
ImplicitUsingsenabled (thedotnet newdefault) - ASP.NET Core shared framework from
Microsoft.NET.Sdk.Web - A capture backend that implements
IHelperBackend
Where it goes#
Save it in the native helper project as LoopbackHelperApi.cs. Construct it with your backend and call StartAsync.
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Sarang Datye
using System.Net;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
public interface IHelperBackend
{
void ConfirmPairing(PairingConfirmRequest request);
void StartCapture(SessionActionRequest request);
CaptureStopResponse StopCapture(SessionActionRequest request);
CaptureStatusResponse CaptureStatus(Guid meetingSessionId);
void Disconnect(SessionActionRequest request);
void Revoke(SessionActionRequest request);
}
public sealed class LoopbackHelperApi(IHelperBackend backend, string host = "127.0.0.1", int port = 6173)
: IAsyncDisposable
{
private WebApplication? _app;
public async Task StartAsync(CancellationToken cancellationToken = default)
{
if (host is not ("127.0.0.1" or "localhost"))
{
throw new InvalidOperationException($"Refusing non-loopback host '{host}'.");
}
var builder = WebApplication.CreateSlimBuilder();
builder.Logging.SetMinimumLevel(LogLevel.Warning);
builder.WebHost.ConfigureKestrel(options => options.Listen(IPAddress.Loopback, port));
var app = builder.Build();
app.Run((RequestDelegate)HandleAsync);
await app.StartAsync(cancellationToken);
_app = app;
}
public async Task StopAsync(CancellationToken cancellationToken = default)
{
if (_app is null)
{
return;
}
await _app.StopAsync(cancellationToken);
await _app.DisposeAsync();
_app = null;
}
private async Task HandleAsync(HttpContext context)
{
var response = await RouteAsync(context);
context.Response.StatusCode = response.StatusCode;
if (response.Body is { Length: > 0 } body)
{
context.Response.ContentType = "application/json";
await context.Response.Body.WriteAsync(body, context.RequestAborted);
}
}
private async Task<HelperResponse> RouteAsync(HttpContext context)
{
try
{
return (context.Request.Method.ToUpperInvariant(), context.Request.Path.Value) switch
{
("GET", "/health") => Json(new { status = "ok" }, 200),
("POST", "/pairing/confirm") => await ConfirmAsync(context),
("POST", "/capture/start") => await StartCaptureAsync(context),
("POST", "/capture/stop") => await StopCaptureAsync(context),
("GET", "/capture/status") => CaptureStatus(context),
("POST", "/pairing/disconnect") => await DisconnectAsync(context),
("POST", "/pairing/revoke") => await RevokeAsync(context),
_ => new HelperResponse(404)
};
}
catch (JsonException)
{
return Error("badRequest", 400);
}
}
private async Task<HelperResponse> ConfirmAsync(HttpContext context)
{
backend.ConfirmPairing(await ReadJsonAsync<PairingConfirmRequest>(context));
return new HelperResponse(200);
}
private async Task<HelperResponse> StartCaptureAsync(HttpContext context)
{
backend.StartCapture(await ReadJsonAsync<SessionActionRequest>(context));
return new HelperResponse(202);
}
private async Task<HelperResponse> StopCaptureAsync(HttpContext context)
{
return Json(backend.StopCapture(await ReadJsonAsync<SessionActionRequest>(context)), 202);
}
private HelperResponse CaptureStatus(HttpContext context)
{
if (!Guid.TryParse(context.Request.Query["meetingSessionId"], out var meetingSessionId))
{
return Error("meetingSessionId is required", 400);
}
return Json(backend.CaptureStatus(meetingSessionId), 200);
}
private async Task<HelperResponse> DisconnectAsync(HttpContext context)
{
backend.Disconnect(await ReadJsonAsync<SessionActionRequest>(context));
return new HelperResponse(200);
}
private async Task<HelperResponse> RevokeAsync(HttpContext context)
{
backend.Revoke(await ReadJsonAsync<SessionActionRequest>(context));
return new HelperResponse(200);
}
private static async Task<T> ReadJsonAsync<T>(HttpContext context)
{
var value = await JsonSerializer.DeserializeAsync<T>(
context.Request.Body,
JsonOptions,
context.RequestAborted);
return value ?? throw new JsonException("Request body decoded to null.");
}
private static HelperResponse Json<T>(T value, int statusCode) =>
new(statusCode, JsonSerializer.SerializeToUtf8Bytes(value, JsonOptions));
private static HelperResponse Error(string message, int statusCode) =>
new(statusCode, Encoding.UTF8.GetBytes($"{{\"error\":\"{message}\"}}"));
public async ValueTask DisposeAsync() => await StopAsync();
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
}
public sealed record PairingConfirmRequest(Guid PairingId, string Code, string Origin, string Nonce);
public sealed record SessionActionRequest(Guid MeetingSessionId, string SessionToken, string Nonce);
public sealed record CaptureStatusResponse(string State, string? Detail = null);
public sealed record CaptureStopResponse(string State, string? Detail, CaptureArtifact? Artifact = null);
public sealed record CaptureArtifact(Guid AudioArtifactId, string FileName, string ContentType, long SizeBytes);
public sealed record HelperResponse(int StatusCode, byte[]? Body = null);
Check that it works#
Run the helper and call GET http://127.0.0.1:6173/health. The response is {"status":"ok"}. A non-loopback host configuration throws before the server starts.