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:
@@ -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("{}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user