Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
98d9988
Add OpenSpec change: minimal-api-mediator-openapi-migration
niemyjski May 26, 2026
36cd1bc
refactor: consolidate Startup.cs into minimal hosting Program.cs
niemyjski May 26, 2026
c22ccaa
feat: add Api infrastructure (endpoints, groups, results, filters)
niemyjski May 26, 2026
9312c10
feat: migrate StatusEndpoints and UtilityEndpoints to Minimal API
niemyjski May 26, 2026
5e2b597
feat: migrate Token, WebHook, and Stripe endpoints to Minimal API
niemyjski May 26, 2026
364841b
Migrate SavedViewController and UserController to Minimal API endpoin…
niemyjski May 26, 2026
17c5167
feat: migrate Project and Organization endpoints to Minimal API
niemyjski May 27, 2026
539e61d
feat: migrate Auth endpoints to Minimal API
niemyjski May 27, 2026
cbe2ac5
Migrate Stack, Admin, Event controllers to Minimal API endpoints
niemyjski May 27, 2026
31643d3
Fix minimal API parity regressions
niemyjski May 27, 2026
4f00649
refactor: remove MVC controllers infrastructure
niemyjski May 27, 2026
b1dc7b6
test: add route manifest and OpenAPI snapshot tests
niemyjski May 27, 2026
a68fbe7
fix: address audit findings — route constraints, validation filter, w…
niemyjski May 27, 2026
a2ae9a2
fix: regenerate endpoint-manifest.json with corrected routes
niemyjski May 27, 2026
730e050
refactor: modernize Job runner to minimal hosting pattern
niemyjski May 27, 2026
1356c67
docs: restore API documentation metadata on all endpoints
niemyjski May 27, 2026
5be3aaf
docs: add missing OpenAPI response codes, parameters, and schema types
niemyjski May 27, 2026
69255d5
fix: restore original OpenAPI tags to match controller-derived tag names
niemyjski May 27, 2026
7b403d5
fix: address PR feedback and CI build failure
niemyjski May 27, 2026
30aed31
fix: disable ValidateOnBuild in test factory for CI
niemyjski May 27, 2026
8c5fcdf
fix: resolve Program type ambiguity and address CodeQL feedback
niemyjski May 27, 2026
6bb1dd4
fix: correct ConfigurationResponseEndpointFilter status code check an…
niemyjski May 27, 2026
07ce2c9
fix: resolve validation parity, security bug, and paging issues
niemyjski May 27, 2026
859addf
fix: resolve flaky CI tests and restore OpenAPI schema parity
niemyjski May 27, 2026
5cf1cac
fix: correct stack endpoint response code metadata
niemyjski May 27, 2026
0d370c3
fix: serialize queue-dependent tests to prevent parallel interference
niemyjski May 27, 2026
06a9a9a
fix: restore OpenAPI schema parity with snake_case naming and documen…
niemyjski May 27, 2026
07838fb
fix(security): add organization access check to OrganizationHandler
niemyjski May 27, 2026
55c268d
feat: migrate TokenHandler to Result<T> return types
niemyjski May 28, 2026
025db45
feat: migrate Stack, Auth, Event, Stripe handlers to Result<T>
niemyjski May 28, 2026
3c88785
chore: remove accidental audit-output files
niemyjski May 28, 2026
57e339e
feat: migrate WebHook, Admin, User, SavedView, Project, Organization …
niemyjski May 28, 2026
2a49058
fix: correct status code mappings to preserve original behavior
niemyjski May 28, 2026
701f9ba
chore: gitignore audit-output directory
niemyjski May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@ debug-storybook.log
.devcontainer/devcontainer-lock.json

*.lscache
audit-output/
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Acceptance Criteria: Minimal API + Mediator + OpenAPI Migration

## Route Preservation

- **SHALL** preserve all existing v2 routes with identical HTTP methods, paths, and parameter binding.
- **SHALL** preserve existing v1 compatibility aliases with identical behavior.
- **SHALL** produce identical response status codes for all success and error cases.
- **SHALL** preserve response body shapes (JSON property names, nesting, types).
- **SHALL** preserve response headers (pagination, configuration version, rate-limit headers).
- **SHALL** preserve query parameter behavior (filtering, sorting, paging, time ranges).

## Authentication and Authorization

- **SHALL** preserve auth/authorization behavior for all endpoints.
- **SHALL** preserve `ApiKeyAuthenticationHandler` behavior (API key via header, query string, and bearer token).
- **SHALL** preserve role-based policies (UserPolicy, GlobalAdminPolicy) on all endpoints.
- **SHALL** preserve anonymous access on endpoints currently marked `[AllowAnonymous]`.

## Middleware

- **SHALL** preserve `ThrottlingMiddleware` behavior (rate limiting, response codes, headers).
- **SHALL** preserve `OverageMiddleware` behavior (plan enforcement).
- **SHALL NOT** replace existing middleware implementations.
- **SHALL NOT** change middleware pipeline ordering for existing middleware.

## Validation and Error Handling

- **SHALL** preserve ProblemDetails shape: `instance` field, `reference-id` extension, `errors` map.
- **SHALL** preserve `lower_underscore` error keys in validation error responses.
- **SHALL** produce 422 for validation failures with errors map.
- **SHALL** produce 401 for unauthenticated requests.
- **SHALL** produce 403 for unauthorized requests.
- **SHALL** produce 404 for not-found resources.

## Patching

- **SHALL** preserve `Delta<T>` patch behavior (partial update semantics, unchanged fields not modified).
- **SHALL NOT** introduce JSON Patch in this change.

## Event Ingestion

- **SHALL** preserve raw event ingestion behavior (multipart, compressed, raw body).
- **SHALL** preserve event submission via API key authentication.
- **SHALL** preserve batch event submission.

## Mediator Pattern

- **SHALL NOT** use generated mediator endpoints (MapMediatorEndpoints) for existing public API routes.
- **SHALL** use Foundatio.Mediator for command/query dispatch from endpoint lambdas.
- **SHALL** register all handlers via DI auto-discovery.

## OpenAPI

- **SHALL** preserve `/docs/v2/openapi.json` serving Scalar docs.
- **SHALL** generate build-time OpenAPI artifact during `dotnet build`.
- **SHALL** add route manifest snapshot tests that fail on route addition/removal/change.
- **SHALL** add OpenAPI snapshot tests that fail on schema drift.

## Architecture

- **SHALL** place all new endpoint code under `src/Exceptionless.Web/Api/`.
- **SHALL** keep v1 legacy aliases in the same endpoint file as the canonical v2 route.
- **SHALL** remove `AddControllers()` and `MapControllers()` after all controllers are migrated.
- **SHALL** delete `Controllers/` folder after all controllers are migrated.

## Testing

- **SHALL** pass all existing integration tests without modification (unless test infrastructure needs updating for host changes).
- **SHALL** update `tests/http/*.http` files if endpoint paths or parameters change (they should not).
234 changes: 234 additions & 0 deletions openspec/changes/minimal-api-mediator-openapi-migration/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# Design: Minimal API + Mediator + OpenAPI Migration

## Architecture Overview

```
HTTP Request
→ ASP.NET Minimal API endpoint (route + auth + filters)
→ Foundatio.Mediator dispatch (message → handler)
→ Handler (reuses Core repositories/services)
→ IResult (typed result with headers/status)
→ Response
```

## File Layout

All new code lives under `src/Exceptionless.Web/Api/`:

```
Api/
ApiEndpoints.cs # Extension method: app.MapApiEndpoints()
ApiEndpointGroups.cs # Shared group configuration (prefix, auth, filters)
Endpoints/ # One file per feature area
Messages/ # Request/response message records
Handlers/ # Mediator handlers (one per feature area)
Middleware/ # ValidationMiddleware, LoggingMiddleware
Filters/ # Endpoint filters (ConfigurationResponse, ApiResponseHeaders)
Results/ # Custom IResult types and mapping helpers
Infrastructure/ # Shared utilities (pagination, time range, etc.)
OpenApi/ # OpenAPI customization and conventions
```

## Endpoint Registration Pattern

### ApiEndpoints.cs

Single extension method called from `Program.cs`:

```csharp
public static class ApiEndpoints
{
public static WebApplication MapApiEndpoints(this WebApplication app)
{
app.MapStatusEndpoints();
app.MapUtilityEndpoints();
app.MapTokenEndpoints();
// ... all feature endpoint groups
return app;
}
}
```

### ApiEndpointGroups.cs

Shared group builder configuration:

```csharp
public static class ApiEndpointGroups
{
public static RouteGroupBuilder MapApiGroup(this IEndpointRouteBuilder routes, string prefix)
{
return routes.MapGroup($"api/v2/{prefix}")
.RequireAuthorization(AuthorizationRoles.UserPolicy)
.AddEndpointFilter<ApiResponseHeadersEndpointFilter>()
.WithOpenApi();
}
}
```

### Individual Endpoint Files

Each `*Endpoints.cs` file:
1. Creates a route group with appropriate prefix and auth.
2. Maps all routes for that feature (GET, POST, PUT, PATCH, DELETE).
3. Includes v1 legacy aliases in the same file as the canonical v2 route.
4. Delegates to Foundatio.Mediator for business logic dispatch.

```csharp
public static class StatusEndpoints
{
public static WebApplication MapStatusEndpoints(this WebApplication app)
{
var group = app.MapApiGroup("");

group.MapGet("about", async (IMediator mediator) =>
{
var result = await mediator.SendAsync(new GetAboutQuery());
return Results.Ok(result);
}).AllowAnonymous();

// ... other status routes
return app;
}
}
```

## Mediator Dispatch Pattern

### Messages

Records in `Messages/*.cs` representing commands and queries:

```csharp
// Messages/StatusMessages.cs
public record GetAboutQuery : ICommand<AboutResponse>;
public record GetQueueStatsQuery : ICommand<QueueStatsResponse>;
public record PostReleaseNotificationCommand(string Message, bool Critical) : ICommand<ReleaseNotification>;
```

### Handlers

Classes in `Handlers/*.cs` that implement `ICommandHandler<TMessage, TResponse>`:

```csharp
// Handlers/StatusHandler.cs
public class StatusHandler :
ICommandHandler<GetAboutQuery, AboutResponse>,
ICommandHandler<GetQueueStatsQuery, QueueStatsResponse>
{
// Inject existing Core services/repositories
private readonly AppOptions _appOptions;
private readonly IQueue<EventPost> _eventQueue;
// ...
}
```

### Handler Reuse of Existing Logic

Handlers do NOT duplicate repository/service logic. They:
1. Accept the message.
2. Call existing `Core` repositories (`IEventRepository`, `IStackRepository`, etc.) and services.
3. Map results to response DTOs or return domain models directly.
4. Return the result (handler does not create HTTP responses).

The endpoint lambda is responsible for mapping handler results to HTTP semantics (status codes, headers, pagination links).

## Validation Strategy

### Automatic Validation (DataAnnotation)

ASP.NET Core Minimal API validates `[AsParameters]` and `[FromBody]` DTOs automatically when `AddEndpointsApiExplorer()` and validation filters are configured. This covers simple required/range/string-length constraints.

### MiniValidation (Complex Cases)

For validation that cannot be expressed with DataAnnotations (cross-field, conditional, post-patch):

```csharp
var (isValid, errors) = MiniValidator.TryValidate(model);
if (!isValid)
return Results.ValidationProblem(errors);
```

Used for:
- Delta<T> patch validation (validate merged model after applying delta).
- Complex cross-field rules.
- Conditional validation based on AppOptions/feature flags.

### Delta<T> Preservation

- `Delta<T>` remains the patch mechanism.
- No JSON Patch introduced.
- After applying delta to the entity, MiniValidation validates the merged result.

## ProblemDetails Centralization

Configure `AddProblemDetails()` with a customizer that ensures:

- `instance` field set to request path.
- `extensions["reference-id"]` set to trace ID.
- `errors` map uses `lower_underscore` keys.
- Validation errors produce 422 with errors map.
- Not-found produces 404.
- Auth failures produce 401/403.

This is configured once in DI and applies to all endpoints.

## OpenAPI Generation

### Runtime

- `Microsoft.AspNetCore.OpenApi` generates `/docs/v2/openapi.json` at runtime.
- Scalar UI served at `/docs` (or existing Scalar path).
- Operation IDs derived from endpoint metadata.

### Build-Time

- `Microsoft.Extensions.ApiDescription.Server` generates `openapi.json` during `dotnet build`.
- Artifact committed or CI-compared for drift detection.
- Snapshot test compares build-time artifact against known-good baseline.

### Route Manifest Tests

- Test enumerates all registered endpoints at startup.
- Compares `{method} {path}` list against a checked-in manifest file.
- Any route addition/removal/change fails the test until manifest is updated.

## Migration Strategy

### Incremental, Controller-by-Controller

1. **Baseline**: Capture OpenAPI snapshot and route manifest from existing controllers.
2. **Infrastructure**: Build Api/ folder, registration, filters, results, infrastructure utilities.
3. **Per-controller migration** (ordered by complexity, simplest first):
- Create endpoint file + messages + handler.
- Wire up in ApiEndpoints.cs.
- Run integration tests against new endpoints.
- Verify OpenAPI snapshot unchanged.
- Remove old controller.
4. **Final cleanup**: Remove `AddControllers()` / `MapControllers()`, delete `Controllers/` folder.

### Coexistence During Migration

During migration, both controllers and new endpoints exist. Route conflicts are avoided by:
- Only activating the new endpoint after the controller is deleted.
- OR: Using a feature flag / conditional registration (prefer delete-and-replace approach).

## Rollback Approach

- Each controller migration is a separate PR.
- Reverting a PR restores the controller and removes the Minimal API endpoint.
- OpenAPI snapshot and route manifest tests confirm the revert is clean.
- No database/index migrations are involved; rollback is purely code.

## Security Considerations

- Auth policies are applied identically via `.RequireAuthorization()`.
- `ApiKeyAuthenticationHandler` remains unchanged in the pipeline.
- Endpoint filters replicate any per-action auth checks from controller action methods.
- No new attack surface introduced.

## Performance Considerations

- Minimal APIs have slightly lower overhead than MVC controllers (no model binding pipeline, no action filters reflection).
- Mediator dispatch adds negligible overhead (in-process, no serialization).
- No performance regression expected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Proposal: Minimal API + Mediator + OpenAPI Migration

## Summary

Migrate all ASP.NET Core MVC controllers in `src/Exceptionless.Web/Controllers/` to Minimal API endpoints backed by Foundatio.Mediator for command/query dispatch, with runtime and build-time OpenAPI generation.

## Why OpenSpec Is Justified

This change affects:

- **Public API behavior** — Every existing route is being re-implemented in a new hosting model.
- **SDK/client compatibility** — Route paths, response shapes, status codes, headers, and auth must remain identical.
- **Middleware ordering** — ThrottlingMiddleware, OverageMiddleware, and endpoint filters must maintain current behavior.
- **OpenAPI contract** — New generation mechanism replaces the existing Swagger setup.
- **Cross-cutting concerns** — Validation, ProblemDetails, pagination, and Delta<T> patching all interact with the new endpoint model.

The scope is large (14 controllers), the compatibility surface is wide, and regression risk without explicit acceptance criteria is high.

## Classification

- **Primary**: Refactor (controller → Minimal API)
- **Secondary**: Infrastructure (Mediator pattern, OpenAPI generation, build-time artifact)

## Affected Areas

| Area | Impact |
|------|--------|
| Backend/API | All public endpoints migrated |
| Tests | New snapshot tests, existing integration tests must pass |
| SDK/client compatibility | Must be zero-breaking-change |
| Docker/deployment | No container changes; build-time OpenAPI artifact added |
| Docs | Scalar docs preserved at /docs/v2/openapi.json |

## Compatibility Risks

| Risk | Mitigation |
|------|-----------|
| Route regression | Route manifest snapshot tests detect any path/method drift |
| Auth bypass | Existing auth policies applied identically; integration tests verify |
| Response shape change | OpenAPI snapshot tests detect schema drift |
| Middleware ordering | Pipeline order preserved; no middleware replaced |
| Validation gap | Automatic validation + MiniValidation covers all current cases |
| Header loss | Endpoint filters replicate current action filters |

## Rollback Plan

1. The migration is incremental (one controller at a time). Each migrated endpoint coexists with the original controller during development.
2. If a regression is detected post-merge, revert the PR that removed the specific controller. The Minimal API endpoint and the controller cannot both be active for the same route, so reverting the controller removal restores prior behavior.
3. OpenAPI snapshot tests and route manifest tests provide immediate CI signal if rollback introduces drift.

## Controllers to Migrate

| Controller | Routes | Priority |
|-----------|--------|----------|
| StatusController | /api/v2/about, queue-stats, notifications/* | Early (simple) |
| UtilityController | /api/v2/search/validate, /api/v2/timezones | Early (simple) |
| TokenController | CRUD for API tokens | Mid |
| SavedViewController | CRUD for saved views | Mid |
| ProjectController | CRUD + config, notifications, integrations | Mid |
| OrganizationController | CRUD + invoices, plans, suspend | Mid |
| StackController | CRUD + mark fixed/critical/snoozed | Mid |
| UserController | CRUD + email verification | Mid |
| WebHookController | CRUD for webhooks | Mid |
| StripeController | Webhook receiver | Mid |
| AuthController | Login, signup, OAuth, forgot-password | Late (complex auth) |
| AdminController | System admin operations | Late |
| EventController | Ingestion, query, count, sessions | Last (highest complexity) |
Loading