fix(management-service): resolve ManagementService-004,006,007,013 — PipeTo dispatch, JsonDocument disposal, unified serialization, endpoint tests; re-triage MS-009

This commit is contained in:
Joseph Doherty
2026-05-16 21:22:01 -04:00
parent da955042aa
commit 57679d49f2
5 changed files with 340 additions and 78 deletions

View File

@@ -77,46 +77,19 @@ public static class ManagementEndpoints
permittedSiteIds);
// 4. Parse command from request body
JsonDocument doc;
try
string body;
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8))
{
doc = await JsonDocument.ParseAsync(context.Request.Body);
}
catch
{
return Results.Json(new { error = "Invalid JSON body.", code = "BAD_REQUEST" }, statusCode: 400);
body = await reader.ReadToEndAsync();
}
if (!doc.RootElement.TryGetProperty("command", out var commandNameElement))
var parse = ParseCommand(body);
if (!parse.Success)
{
return Results.Json(new { error = "Missing 'command' field.", code = "BAD_REQUEST" }, statusCode: 400);
return Results.Json(new { error = parse.ErrorMessage, code = parse.ErrorCode }, statusCode: 400);
}
var commandName = commandNameElement.GetString();
if (string.IsNullOrWhiteSpace(commandName))
{
return Results.Json(new { error = "Empty 'command' field.", code = "BAD_REQUEST" }, statusCode: 400);
}
var commandType = ManagementCommandRegistry.Resolve(commandName);
if (commandType == null)
{
return Results.Json(new { error = $"Unknown command: '{commandName}'.", code = "BAD_REQUEST" }, statusCode: 400);
}
object command;
try
{
var payloadElement = doc.RootElement.TryGetProperty("payload", out var p)
? p
: JsonDocument.Parse("{}").RootElement;
command = JsonSerializer.Deserialize(payloadElement.GetRawText(), commandType,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
}
catch (Exception ex)
{
return Results.Json(new { error = $"Failed to deserialize payload: {ex.Message}", code = "BAD_REQUEST" }, statusCode: 400);
}
var command = parse.Command!;
// 5. Dispatch to ManagementActor
var holder = context.RequestServices.GetRequiredService<ManagementActorHolder>();
@@ -148,4 +121,68 @@ public static class ManagementEndpoints
_ => Results.Json(new { error = "Unexpected response.", code = "INTERNAL_ERROR" }, statusCode: 500)
};
}
/// <summary>
/// Result of parsing a management request body into a strongly-typed command.
/// </summary>
public readonly record struct CommandParseResult(
bool Success, object? Command, string? ErrorMessage, string? ErrorCode)
{
public static CommandParseResult Ok(object command) => new(true, command, null, null);
public static CommandParseResult Fail(string message) => new(false, null, message, "BAD_REQUEST");
}
/// <summary>
/// Parses a management request body — a JSON object with a <c>command</c> name and an
/// optional <c>payload</c> — into the strongly-typed command record. The parsed
/// <see cref="JsonDocument"/> is disposed deterministically and the missing-payload
/// case does not allocate a throwaway document (finding ManagementService-006).
/// </summary>
public static CommandParseResult ParseCommand(string body)
{
using JsonDocument doc = ParseDocument(body, out var parseError);
if (parseError != null)
return CommandParseResult.Fail(parseError);
if (!doc.RootElement.TryGetProperty("command", out var commandNameElement))
return CommandParseResult.Fail("Missing 'command' field.");
var commandName = commandNameElement.GetString();
if (string.IsNullOrWhiteSpace(commandName))
return CommandParseResult.Fail("Empty 'command' field.");
var commandType = ManagementCommandRegistry.Resolve(commandName);
if (commandType == null)
return CommandParseResult.Fail($"Unknown command: '{commandName}'.");
try
{
// Missing payload: deserialize from the empty-object literal rather than
// allocating (and leaking) a throwaway JsonDocument.
var payloadJson = doc.RootElement.TryGetProperty("payload", out var p)
? p.GetRawText()
: "{}";
var command = JsonSerializer.Deserialize(payloadJson, commandType,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
return CommandParseResult.Ok(command);
}
catch (Exception ex)
{
return CommandParseResult.Fail($"Failed to deserialize payload: {ex.Message}");
}
}
private static JsonDocument ParseDocument(string body, out string? error)
{
try
{
error = null;
return JsonDocument.Parse(body);
}
catch
{
error = "Invalid JSON body.";
return JsonDocument.Parse("{}");
}
}
}