Skip to content

OpenSpec: Refactor request router into middleware pipeline#22

Merged
yetanotherchris merged 7 commits intomainfrom
openspec/refactor-request-router
Feb 28, 2026
Merged

OpenSpec: Refactor request router into middleware pipeline#22
yetanotherchris merged 7 commits intomainfrom
openspec/refactor-request-router

Conversation

@yetanotherchris
Copy link
Owner

@yetanotherchris yetanotherchris commented Feb 28, 2026

OpenSpec: Refactor Request Router

Problem

DynamoDbRequestRouter handles 5 responsibilities in one class: health check, HTTP validation, operation dispatch, JSON serialization, and error formatting. This violates SRP and makes the class the dumping ground for unrelated changes.

Proposed Solution

Decompose into focused classes using ASP.NET Core's middleware pipeline:

Class Responsibility
HealthCheckHandler Standalone GET / endpoint via MapGet
DynamoDbErrorMiddleware Cross-cutting error formatting (DynamoDbException, JsonException, TransactionCanceledException)
DynamoDbValidationMiddleware HTTP method/path checks, X-Amz-Target header parsing
DynamoDbRequestRouter Pure operation dispatch (~35 lines, down from 114)

Pipeline

GET / → HealthCheckHandler (short-circuits) POST / → ErrorMiddleware → ValidationMiddleware → Router (dispatch)

Key Decisions

  • Middleware over static helpers — error handling wraps all downstream automatically
  • Throw from validation, catch in error middleware — single error formatting location
  • Keep switch dispatch — appropriate for RPC-style protocol with 14 operations; no handler registry needed
  • No operation class changes — only the server layer is refactored

Files

See openspec/changes/refactor-request-router/ for full proposal, design, and tasks.

Design Feedback

The following changes were made to the initial proposal following review by Chris:

Health check: drop the HealthCheckHandler class
The initial draft proposed a HealthCheckHandler static class with a MapHealthCheck extension method. Chris pointed out this is a one-liner and doesn't warrant a separate class — it should live directly in Program.cs. Accepted: health check is now a MapHealthChecks call inline in Program.cs.

Health check: use the built-in ASP.NET Core framework
Chris noted that ASP.NET Core has built-in health check extension methods (AddHealthChecks() / MapHealthChecks()). The initial draft used a raw MapGet. Accepted: switched to builder.Services.AddHealthChecks() + app.MapHealthChecks() with a custom ResponseWriter, which is more extensible and idiomatic.

Health check: add /healthz
Chris requested the health check also respond at the standard /healthz path, in addition to /. Accepted: both paths are mapped with a shared HealthCheckOptions instance.

HttpContext.Items for operation name: remove it
The initial design stored the parsed operation name in context.Items["DynamoDb.Operation"] in the validation middleware and read it back in the router. Chris flagged this as unpleasant (a stringly-typed dictionary lookup with a cast). Accepted: the router reads X-Amz-Target directly — the validation middleware has already guaranteed it is valid, so re-parsing is safe and clearer.

Command pattern instead of Dispatch<TReq, TRes>
The initial design retained a generic Dispatch<TReq, TRes>(Stream body, Func<TReq, TRes> handler) method inside the router, with a 14-arm switch expression passing operation methods as Func delegates. Chris observed this was an unnecessarily dynamic/functional style for C# — each operation should be a proper typed class. Accepted: replaced with an IDynamoDbCommand interface, a DynamoDbCommand<TRequest, TResponse> abstract base class handling serialization, and 14 concrete command classes. The router becomes a dictionary dispatcher with no knowledge of specific operations.

yetanotherchris and others added 7 commits February 28, 2026 17:48
Decompose DynamoDbRequestRouter into focused classes using ASP.NET Core
middleware pipeline:

- HealthCheckHandler: standalone GET / endpoint
- DynamoDbErrorMiddleware: cross-cutting error formatting
- DynamoDbValidationMiddleware: HTTP/header validation
- DynamoDbRequestRouter: pure operation dispatch

Includes proposal, design, and tasks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplify design — the health check is a one-liner, a dedicated
HealthCheckHandler class is overkill.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace MapGet with AddHealthChecks() + MapHealthChecks('/') and a
custom ResponseWriter. Extensible for future IHealthCheck implementations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Map health checks to both / (backward compat) and /healthz (standard
convention) using shared HealthCheckOptions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Assign context.Request.Body to a local var instead of repeating it
  14 times in the switch expression
- Router reads X-Amz-Target header directly instead of using
  HttpContext.Items magic string dictionary
- Validation middleware still validates the header; router just parses it

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce IDynamoDbCommand interface + DynamoDbCommand<TRequest,TResponse>
abstract base class. Each operation becomes a typed command class that owns
its own deserialization and serialization. Router becomes a dictionary
dispatcher with no knowledge of specific operations.

14 command classes grouped into 5 files matching existing operation class
grouping. Router constructor changes from 5 operation class params to
IEnumerable<IDynamoDbCommand>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Design Feedback section recording five changes made during review:
- Drop HealthCheckHandler class (inline into Program.cs)
- Use built-in AddHealthChecks()/MapHealthChecks() framework
- Add /healthz alongside /
- Remove HttpContext.Items for operation name (read header directly)
- Replace Dispatch<TReq,TRes> switch with Command pattern

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yetanotherchris yetanotherchris merged commit 3b9f41c into main Feb 28, 2026
1 check passed
@yetanotherchris yetanotherchris deleted the openspec/refactor-request-router branch February 28, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant