Skip to content

refactor: migrate Exceptionless.Web to Minimal APIs with Foundatio.Mediator#2257

Open
niemyjski wants to merge 76 commits into
mainfrom
niemyjski/minimal-api-mediator-openapi-migration
Open

refactor: migrate Exceptionless.Web to Minimal APIs with Foundatio.Mediator#2257
niemyjski wants to merge 76 commits into
mainfrom
niemyjski/minimal-api-mediator-openapi-migration

Conversation

@niemyjski

@niemyjski niemyjski commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Migrate all Exceptionless.Web controllers to Minimal API endpoints using Foundatio.Mediator for command/query dispatch.

What Changed

  • Architecture: Controllers -> thin endpoint functions that delegate to Mediator handlers
  • Entry Point: Consolidated Startup.cs into single minimal hosting Program.cs
  • Job Runner: Modernized to WebApplication.CreateBuilder() minimal hosting
  • OpenAPI: Preserved documentation, tags, parameters, response codes via endpoint/OpenAPI transformers
  • Testing: Endpoint manifest + OpenAPI snapshot tests protect route/auth/contract parity

Current Audit Evidence

Area Evidence Status
Merge-forward Branch contains latest origin/main merge commit 6b0c0710; origin/main is 5bde93d PASS
Route parity main controller manifest and branch endpoint manifest both have 209 entries / 208 unique method+route keys PASS
Missing/extra public routes 0 missing, 0 extra by method+route comparison PASS
OpenAPI parity main and branch both expose 120 OpenAPI paths / 156 operations PASS
Public API snapshots OpenApiSnapshotTests, EndpointManifestTests, ValidationSnapshotTests passed in controller/API suite PASS
Controller/API tests dotnet test ... --filter-namespace Exceptionless.Tests.Controllers passed 748/748 PASS
Middleware tests dotnet test ... --filter-namespace Exceptionless.Tests.Utility.Handlers passed 23/23 PASS
Full backend tests dotnet test tests/Exceptionless.Tests/Exceptionless.Tests.csproj passed 2347, skipped 2 explicit/perf tests, failed 0 PASS
Frontend tests/builds Svelte unit 293/293, Svelte build, Svelte check, Angular build PASS
OpenSpec openspec validate minimal-api-mediator-openapi-migration --strict --no-interactive passes after adding audit delta PASS
Local dogfood Svelte login/stacks/events and Angular login/frequent/events loaded locally with data PASS with notes

Dogfood notes:

  • Svelte emitted two state_proxy_equality_mismatch warnings, but no browser page errors.
  • Legacy Angular emitted the existing local Exceptionless-client ApiKey is not set console error while event submission is disabled; no browser page errors were reported.

Main vs Branch Performance

Idle-machine rerun on 2026-07-08 UTC, local AppHost benchmark, browser fetch against direct Aspire API origins, Debug build, shared local Docker infra, fresh local account/project/sample data per run, 10 warmups + 50 measured samples per endpoint. Values are median / p95 milliseconds. Negative delta means the branch is faster. Main ref 5bde93d; branch head 9808110.

Endpoint main median branch median median delta main p95 branch p95 p95 delta
GET /api/v2/users/me 2.6 2.9 +11.5% slower 3.5 5.6 +60.0% slower
GET /api/v2/organizations 4.9 5.2 +6.1% slower 5.9 7.2 +22.0% slower
GET /api/v2/projects?organizationId=... 8.5 7.8 -8.2% faster 10.5 8.9 -15.2% faster
GET /api/v2/organizations/{id}/saved-views/events 6.5 6.0 -7.7% faster 9.2 9.4 +2.2% slower
GET /api/v2/projects/{id}/events?limit=10 9.8 9.5 -3.1% faster 11.1 14.5 +30.6% slower
GET /api/v2/events/count?... 11.6 10.1 -12.9% faster 23.8 11.6 -51.3% faster
GET /api/v2/events/{id} 8.5 7.1 -16.5% faster 16.6 7.9 -52.4% faster
GET /api/v2/stacks/{id} 2.2 2.1 -4.5% faster 2.8 2.5 -10.7% faster
GET /api/v2/projects/config 1.3 1.2 -7.7% faster 1.7 1.5 -11.8% faster
GET /api/v2/projects/config?v=current 1.2 1.2 0.0% equal 1.6 1.4 -12.5% faster

Performance verdict:

  • Branch is faster or equal on 8/10 median paths and 7/10 p95 paths in the idle rerun.
  • Aggregate endpoint medians improved from 57.1 ms to 53.1 ms across the measured set, about 7.0% faster overall by summed medians.
  • The branch wins most clearly on event/count and event-detail tail latency; the previous projects-list slowdown did not reproduce.
  • Watch items: users/me and organizations are slightly slower in median and p95, and events-list median is faster but p95 had branch outliers.
  • ProjectConfigMiddleware still looks correct to keep: the hot path remains about 1.2 ms median and tied/faster at p95, so removing it for mediator routing would not be justified by this benchmark.
  • Caveat: the benchmark seeds fresh sample data per run; response byte counts are not a formal contract diff. In this run, branch event-detail bytes were smaller than main, so public API parity should continue to rely on the snapshot/manifest tests above rather than benchmark payload size alone.

Coverage Report

Same filtered controller/API namespace on current main vs branch:

Target Tests Overall line Overall branch Focus area
main controller suite 722 passed 60.25% 49.84% MVC controllers line 81.30%, branch 69.15%
branch controller/API suite 748 passed 60.73% 49.46% endpoint files line 97.01%, handlers line 79.85%, filters line 100%
branch middleware suite 23 passed 18.98% suite-wide 11.06% suite-wide ProjectConfig 100%, Throttling 84.51%, Overage 94.20%, Heartbeat 79.59%

Important coverage caveat: this is not 100% line/branch coverage. Remaining gaps are mostly handler edge paths, ApiResults helper overloads, CurrentUserAccessor, ApiValidation, and middleware edge branches. No optimization beyond the OpenSpec documentation fix was made in this audit.

Key Invariants Preserved

Invariant Status
All public routes PASS: 208 unique method+route keys match main exactly
Auth policies on endpoints PASS: covered by endpoint manifest snapshot
OpenAPI path/operation count PASS: 120 paths / 156 operations on both sides
Route constraints (token, tokens, objectid) PASS: covered by endpoint/OpenAPI snapshots
ProblemDetails shape (instance, reference-id, errors, lower_underscore keys) PASS: covered by validation tests/snapshots
JsonPatch/Delta replacement behavior PASS: covered by JSON patch tests/snapshots
ThrottlingMiddleware / OverageMiddleware PASS: unchanged and middleware tests pass
ProjectConfigMiddleware PASS: faster locally and 100% line/branch covered in middleware suite
Health checks (/health, /ready) PASS: preserved

Structure

src/Exceptionless.Web/Api/
  Endpoints/       thin HTTP adapters
  Messages/        command/query records
  Handlers/        use-case logic ported from controller actions
  Filters/         AutoValidationEndpointFilter, ConfigurationResponseEndpointFilter
  Results/         ApiResults, PagedResult, mapper helpers
  Infrastructure/  Pagination, TimeRangeParser, CurrentUserAccessor, validation helpers

Breaking Changes

None found in the latest audit. Public route/method parity matches origin/main; OpenAPI snapshot tests pass; local Svelte and Angular dogfood paths load with data.

Known Acceptable Differences

  • StringStringValuesKeyValuePair schema removed (MVC model binding artifact) -> ProblemDetails schema added (from .ProducesProblem())
  • Versioned subscribe route /api/v{apiVersion:int}/webhooks/subscribe lacks =2 default (Minimal API limitation, canonical /api/v2/webhooks/subscribe route exists)

niemyjski and others added 18 commits May 26, 2026 17:15
Planning artifacts for migrating Exceptionless.Web controllers to
Minimal APIs with Foundatio.Mediator dispatch, preserving all existing
API behavior.

Change deliverables:
- proposal.md: justification, classification, rollback plan
- design.md: architecture, endpoint/mediator/handler patterns
- tasks.md: 19 ordered migration tasks with verification steps
- acceptance.md: SHALL/SHALL NOT acceptance criteria
- risks.md: 9 risks with mitigation strategies

New specs (testable SHALL statements):
- api-architecture: endpoint registration, mediator dispatch, DI
- api-contract: route/response/header preservation
- api-validation: DataAnnotation + MiniValidation
- api-problem-details: error response shape
- api-middleware: throttling, overage, filters, pipeline ordering
- api-openapi: runtime/build-time generation, snapshot tests
- api-patching: Delta<T> preservation, no JSON Patch

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merge all service registrations and middleware pipeline into single Program.cs
- Use WebApplication.CreateBuilder() minimal hosting pattern
- Add Foundatio.Mediator 1.2.1 package reference
- Add Microsoft.Extensions.ApiDescription.Server for build-time OpenAPI
- Add stub MapApiEndpoints() extension for future endpoint registrations
- Update AppWebHostFactory to use WebApplicationFactory<Program>
- Remove Startup.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ApiEndpointGroups: shared route group builder with auth policy
- ApiResults: OkWithLinks, OkWithResourceLinks, Permission, WorkInProgress helpers
- Pagination: limit/page/skip helpers extracted from base controller
- TimeRangeParser: time range parsing extracted from base controller
- CurrentUserAccessor: HttpContext user helpers
- ConfigurationResponseEndpointFilter: config version header filter
- ApiResponseHeadersEndpointFilter: common response headers
- ApiValidation: MiniValidation wrapper for endpoint validation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create Messages/StatusMessages.cs with command/query records
- Create Handlers/StatusHandler.cs and UtilityHandler.cs with mediator handlers
- Create Endpoints/StatusEndpoints.cs and UtilityEndpoints.cs
- Remove StatusController.cs and UtilityController.cs
- Wire up MapApiEndpoints() in ApiEndpoints.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- TokenEndpoints: full CRUD with org/project scoped routes
- WebHookEndpoints: CRUD plus Zapier subscribe/unsubscribe/test
- StripeEndpoints: webhook receiver with signature validation
- All use Foundatio.Mediator handler pattern
- Remove TokenController, WebHookController, StripeController

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ts with Foundatio.Mediator

Replace MVC controllers with the same Minimal API + Mediator pattern
used by Token, WebHook, and Status endpoints. Each controller is split
into Messages (records), Handler (business logic), and Endpoints
(HTTP routing via IMediator).

Preserves all routes, route constraints (:objectid, :token, :minlength),
auth policies (User, GlobalAdmin), named routes (GetSavedViewById,
GetUserById), and behavior including predefined saved view management,
email verification, admin role management, and rate-limited email updates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ProjectEndpoints: full CRUD, config, notifications, integrations, Slack
- OrganizationEndpoints: full CRUD, invoices, plans, suspend
- Preserve all routes, auth policies, route names
- Remove ProjectController.cs and OrganizationController.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- AuthEndpoints: login, signup, OAuth, forgot-password, change-password
- Preserve AllowAnonymous on public auth routes
- Port complete OAuth flow (Google, Facebook, GitHub, Microsoft)
- Remove AuthController.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace MVC controllers with Foundatio.Mediator-based Minimal API
endpoints following the established pattern (Messages/Handlers/Endpoints).
All routes, authorization policies, and route names are preserved.

- AdminController → AdminMessages + AdminHandler + AdminEndpoints
- StackController → StackMessages + StackHandler + StackEndpoints
- EventController → EventMessages + EventHandler + EventEndpoints
- Update ApiEndpoints.cs to register new endpoint groups
- Fix ControllerManifestTests assembly reference (no controllers remain)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove AddControllers() and MapControllers() from Program.cs
- Remove AddAutoValidation() (MVC-specific filter)
- Remove ExceptionlessApiController, ReadOnlyRepositoryApiController, RepositoryApiController base classes
- Keep shared types (PermissionResult, TimeInfo, WorkInProgressResult, ModelActionResults)
- Update ControllerManifestTests to verify no MVC controllers remain
- Full solution builds with 0 errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- EndpointManifestTests: verifies all endpoint classes are registered
- OpenApiSnapshotTests: lightweight test app for OpenAPI document verification
- MinimalApiTestApp: shared test host without Elasticsearch dependency
- SnapshotTestHelper: shared snapshot comparison utility
- Remove old OpenApiControllerTests (replaced by snapshot approach)
- Generate initial endpoint-manifest.json and openapi.json baselines

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ebhook subscribe route, user-agent

- Restore :token/:tokens route constraints on token endpoints
- Add canonical api/v2/webhooks/subscribe route (was only versioned)
- Create AutoValidationEndpointFilter for Minimal API auto-validation
- Register auto-validation filter on all endpoint groups
- Remove dead ApiEndpointGroups.cs
- Fix UserAgent header regression: prefer X-Exceptionless-Client over User-Agent
- Fix Stripe trailing slash: map POST directly without empty-string sub-route
- Delete obsolete controller-manifest.json test fixture

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Routes now match pre-migration manifest (184 endpoints, all constraints preserved).
Only remaining diff: versioned subscribe route template lacks =2 default
(Minimal API limitation; covered by canonical api/v2/webhooks/subscribe route).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace Host.CreateDefaultBuilder()/ConfigureWebHostDefaults() with
WebApplication.CreateBuilder() for consistency with the web project.
Preserves all behavior: health checks, Serilog, APM, job registration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port all XML doc summaries, parameter descriptions, and response
descriptions from the old MVC controllers to Minimal API endpoints
using .WithSummary() and .WithMetadata(EndpointDocumentation) with a
custom IOpenApiOperationTransformer.

Results vs old spec (128/348/244 target):
- Summaries: 128/128 (100%)
- Parameter descriptions: 298/348 (86% - gap is from params not in
  lambda signatures like headers and manual query params)
- Response descriptions: 266 total responses documented (exceeds 244)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend EndpointDocumentationOperationTransformer to support injecting
additional parameters (e.g. User-Agent header, query string arrays).

Add Produces<T>() and ProducesProblem() declarations across all endpoint
files to document response types and error codes. This brings coverage to:
- Summaries: 128 (unchanged)
- Parameters: 409 (was 287, added 122)
- Response codes: 353 (was 231, added 122)
- Schemas: 49 (was 43, added 6)

Update snapshot test assertion from 200 to 202 for user-description
endpoint to match its actual Accepted semantics.
Change endpoint group tags from plural to singular to match the old MVC
controller-derived tags (Event, Organization, Project, Stack, User, etc.).
Add explicit WithTags to Token, WebHook groups and all v1 endpoints that
previously inherited tags from their controller class.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@niemyjski niemyjski requested a review from Copilot May 27, 2026 03:46
@niemyjski niemyjski self-assigned this May 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Comment thread src/Exceptionless.Job/Program.cs Fixed
Comment thread src/Exceptionless.Web/Api/Endpoints/StripeEndpoints.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/EventHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/EventHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/StackHandler.cs Fixed
Comment thread src/Exceptionless.Web/Program.cs Fixed
Comment thread src/Exceptionless.Web/Api/Filters/AutoValidationEndpointFilter.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/EventHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/StackHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/WebHookHandler.cs Fixed
- Disable ValidateOnBuild in WebApplication.CreateBuilder since the
  service graph uses lambda factories (queues, caching, Elasticsearch)
  that resolve dependencies at runtime via IServiceProvider. The old
  Generic Host path did not enable this validation.
- Add using/dispose to StreamReader in StripeEndpoints
- Add using/dispose to MemoryStream in EventHandler
- Add using/dispose to ScopedCacheClient in EventHandler and StackHandler
- Refactor AutoValidationEndpointFilter to use Where() filtering
- Refactor DeleteEvents/DeleteStacks to use LINQ Where() instead of
  mutating a list inside a foreach loop

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/Exceptionless.Web/Program.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/AuthHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Filters/AutoValidationEndpointFilter.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/UserHandler.cs Fixed
Comment thread src/Exceptionless.Web/Program.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/EventHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/AuthHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/AdminHandler.cs Fixed
The ValidateOnBuild=false in Program.cs via builder.Host.UseDefaultServiceProvider()
does not take effect in the minimal hosting model when used with WebApplicationFactory.
The ConfigureHostBuilder stores but may not replay service provider options.

Fix: Add builder.UseDefaultServiceProvider() in AppWebHostFactory.ConfigureWebHost
where the IWebHostBuilder properly replaces the service provider factory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@niemyjski niemyjski force-pushed the niemyjski/minimal-api-mediator-openapi-migration branch from ecc217a to 30aed31 Compare May 27, 2026 04:26
Comment thread src/Exceptionless.Web/Program.cs Fixed
Comment thread src/Exceptionless.Web/Program.cs Fixed
@niemyjski niemyjski requested a review from Copilot May 27, 2026 12:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

@niemyjski niemyjski requested a review from Copilot May 27, 2026 13:34
Comment thread src/Exceptionless.Web/Api/Handlers/EventHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/OrganizationHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/EventHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/AdminHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/OrganizationHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/AdminHandler.cs Fixed
…al-api-mediator-openapi-migration

# Conflicts:
#	src/Exceptionless.Web/Api/Handlers/SavedViewHandler.cs
#	src/Exceptionless.Web/Controllers/AdminController.cs
#	src/Exceptionless.Web/Controllers/EventController.cs
#	src/Exceptionless.Web/Controllers/OrganizationController.cs
#	src/Exceptionless.Web/Controllers/ProjectController.cs
#	src/Exceptionless.Web/Controllers/StackController.cs
#	src/Exceptionless.Web/Controllers/StatusController.cs
#	src/Exceptionless.Web/Startup.cs
#	src/Exceptionless.Web/Utility/Delta/Delta.cs
#	src/Exceptionless.Web/Utility/Delta/DeltaJsonConverter.cs
#	tests/Exceptionless.Tests/AppWebHostFactory.cs
#	tests/Exceptionless.Tests/Controllers/Data/openapi.json
#	tests/Exceptionless.Tests/Controllers/EventControllerTests.cs
#	tests/Exceptionless.Tests/Controllers/OpenApiSnapshotTests.cs
#	tests/Exceptionless.Tests/Serializer/SnakeCaseLowerNamingPolicyTests.cs
Comment thread src/Exceptionless.Job/Program.cs Fixed
Comment thread src/Exceptionless.Web/Api/Filters/AutoValidationEndpointFilter.cs
Comment thread src/Exceptionless.Web/Api/Handlers/WebHookHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/AuthHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/AuthHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/EventHandler.cs Fixed
Comment thread src/Exceptionless.Web/Api/Handlers/UtilityHandler.cs Fixed
ejsmith added 7 commits June 22, 2026 09:31
# Conflicts:
#	src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs
#	src/Exceptionless.Web/Controllers/EventController.cs
#	tests/Exceptionless.Tests/Controllers/Data/openapi.json
# Conflicts:
#	src/Exceptionless.Web/Controllers/AdminController.cs
@ejsmith

ejsmith commented Jun 25, 2026

Copy link
Copy Markdown
Member

Fixed the partial-update compatibility issue in 113c44b.

What changed:

  • Update endpoints now accept both RFC 6902 JSON Patch arrays and legacy object-shaped partial bodies before dispatching the same mediator update messages.
  • Kept OpenAPI request-body metadata as JSON Patch and restored explicit endpoint display names so endpoint/OpenAPI snapshots stay stable.
  • Reused the same shared parser for projects, tokens, users, organizations, and saved views.

Verification:

  • dotnet build tests\Exceptionless.Tests\Exceptionless.Tests.csproj --configuration Release --no-restore --verbosity minimal /m:1
  • dotnet test tests\Exceptionless.Tests\Exceptionless.Tests.csproj --configuration Release --no-build -- --filter-class Exceptionless.Tests.Controllers.TokenControllerTests --filter-class Exceptionless.Tests.Controllers.UserControllerTests --filter-class Exceptionless.Tests.Controllers.OrganizationControllerTests --filter-class Exceptionless.Tests.Controllers.SavedViewControllerTests
  • dotnet test tests\Exceptionless.Tests\Exceptionless.Tests.csproj --configuration Release --no-build -- --filter-class Exceptionless.Tests.Controllers.OpenApiSnapshotTests --filter-class Exceptionless.Tests.Controllers.EndpointManifestTests --filter-class Exceptionless.Tests.Controllers.ControllerManifestTests

Note: the snapshot run on Windows required temporary local LF normalization of AuthEndpoints.cs because raw string descriptions emit CRLF from a CRLF checkout; that file was restored and is not part of the commit.

Comment thread src/Exceptionless.Web/Api/Handlers/WebHookHandler.cs Fixed
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.AppHost 39% 41% 136
Exceptionless.Core 71% 64% 8644
Exceptionless.Insulation 23% 23% 205
Exceptionless.Web 82% 65% 6578
Summary 75% (20797 / 27877) 64% (9339 / 14680) 15563

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.

3 participants