Skip to content

Keep only setup extension classes in Microsoft's namespaces (#198) - #250

Merged
siebe41 merged 3 commits into
mainfrom
sprint/api/A01-builder-namespace
Oct 20, 2026
Merged

siebe41 merged 3 commits into
mainfrom
sprint/api/A01-builder-namespace

Conversation

@siebe41

@siebe41 siebe41 commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Decisions for Andrew

  1. The namespace scheme. I chose:

    • Microsoft.Extensions.DependencyInjection: every setup extension class, on IServiceCollection and on SemanticRouterBuilder, in every package (SemanticRouterServiceCollectionExtensions, SqliteStateBuilderExtensions, OnnxEmbeddingsBuilderExtensions, CopilotConnectorBuilderExtensions). ASP.NET Core does the same for its own builder types (IHttpClientBuilder, IHealthChecksBuilder, IMvcBuilder), and the Web SDK's implicit using means .UseSqliteState(), .UseOnnxEmbeddings() and .AddCopilotConnector() keep working with no using.
    • Microsoft.AspNetCore.Builder: only the three Use*/Map* classes (PromptRouterApplicationBuilderExtensions, RouterDashboardEndpoints, RouterForwardedHeadersExtensions).
    • Everything else, options included, goes in the package's own namespace.

    Alternative: the issue's example, with all SemanticRouterBuilder extensions in SemanticRouter.AspNetCore. Every app would then need using SemanticRouter.AspNetCore; to call them, and Sqlite, Onnx and Copilot would put a class in another package's namespace, which breaks the issue's own "each package's public types live in its own namespace" criterion. With my choice, only the Sqlite extension class moves; Onnx and Copilot already follow the rule.

  2. The classify, /v1/models and count_tokens DTOs became internal instead of moving to a public SemanticRouter.AspNetCore.Http. Nothing outside the package uses them (samples, Hosting, Desktop and tests checked), and their contract is the JSON the endpoints return. Making them public again later is an addition, not a break. Alternative: keep them public in SemanticRouter.AspNetCore.Http, for apps that want PromptClassificationResponse.From(plan) in their own endpoints.

  3. There are 29 nested dashboard records, not the issue's 25. The four PriceLookup* records were added after the audit, and they are internal now too.

  4. MapRouterDashboard gets a new binary signature, because the type of its configure parameter (Action<RouterDashboardOptions>) moved namespace. The source still compiles when the caller passes a lambda. One that names RouterDashboardOptions needs using SemanticRouter.AspNetCore;.

What changed

Removed from the public API, or moved (all suppressed in the CompatibilitySuppressions.xml this PR adds):

SemanticRouter.AspNetCore

  • Moved Microsoft.AspNetCore.Builder.RouterDashboardOptions to SemanticRouter.AspNetCore.RouterDashboardOptions, and gave it its own file, Dashboard/RouterDashboardOptions.cs.
  • Changed the MapRouterDashboard(IEndpointRouteBuilder, string, Action<RouterDashboardOptions>) signature (CP0002), because of the move above.
  • Made internal the 29 nested records of RouterDashboardEndpoints: SessionResponse, SignInRequest, ValidationResult, ConflictResponse, UsageResponse, HistoryResponse, ActivityResponse, ActivitySummary, MemoryMapResponse, MemoryNodeResponse, PriceLookupRequest, PriceLookupModel, PriceLookupResponse, PriceLookupResult, EmbeddingTestRequest, EmbeddingTestResponse, DiscoverResponse, LineupModel, ThemesResponse, StatusResponse, EmbeddingsStatus, IndexStatus, EmbeddingModelStatus, TierStatus, ModelStatus, ConnectorStatus, RouteStatus, GatewayStatus and ConnectorLatency. (The PriceLookup* four were not in the 0.5.0 baseline, so they have no suppression.)
  • Made internal and moved to SemanticRouter.AspNetCore (Middleware/RouterEndpointModels.cs): ClassifyPromptRequest, PromptClassificationResponse (with its nested RouteCandidate and ModelChoice), RouterModelInfo, RouterModelList and TokenCountResponse.

SemanticRouter.Storage.Sqlite

  • Moved SemanticRouter.AspNetCore.SqliteStateOptions to SemanticRouter.Storage.Sqlite.SqliteStateOptions.
  • Moved SemanticRouter.AspNetCore.SqliteStateBuilderExtensions to Microsoft.Extensions.DependencyInjection.SqliteStateBuilderExtensions, in its own file. The internal store classes moved to SemanticRouter.Storage.Sqlite as well.

Unchanged: Core, Embeddings.Onnx and Connectors.Copilot already followed the scheme.

Also:

  • Docs: the convention is in docs/architecture.md#namespaces, with a guideline line in CONTRIBUTING.md, and docs/dashboard.md names the options' namespace.
  • Changelog: a Changed line in CHANGELOG.md.
  • Test: PublicApiNamespaceTests enforces the convention by reflection over all five library assemblies:
    • Microsoft namespaces may hold only non-nested static extension classes.
    • Every other public type must be in its package's namespace and not in a sibling's.
    • The HTTP bodies must not be public.

Why

#198: the Web SDK imports Microsoft.AspNetCore.Builder in every file, so 33 DTO and options types (now 36 with the price lookup) were in scope in every app that uses the package. Every new dashboard field was also a binary breaking change of a NuGet package. Fixing this before 1.0 is the only time it is cheap.

How it was verified

On Windows, on a machine busy with the other sprint lanes:

  • dotnet build -c Release: succeeded, 0 warnings.
  • dotnet format --verify-no-changes: clean.
  • dotnet pack -c Release -p:ApiCompatGenerateSuppressionFile=true: wrote the two suppression files. A second dotnet pack -c Release of AspNetCore and Storage.Sqlite, with the validation semaphores deleted, passes package validation against 0.5.0.
  • dotnet test -c Release (full suite, 1,411 tests): 1,407 passed, 4 failed. None of the four is in code this PR touches, and all four pass when run alone:
    • MemoryMapTests.Event_stream_announces_routed_requests (10 s SSE timeout), ServiceTests.The_router_records_itself_while_it_runs_and_reports_an_address_in_use and DesktopTests.The_tray_reads_the_routers_own_status (HttpClient timeouts) timed out under the load.
    • SecretsTests.Literal_secrets_move_to_the_store_and_resolve_back expects OPENAI_API_KEY to be unset, and it is set on this PC.
  • Those four plus PublicApiNamespaceTests (11 cases), rerun with OPENAI_API_KEY unset: 15/15 passed.

Other OSes: nothing here depends on paths, processes or the shell. It changes only namespaces, accessibility and file layout, so Linux and macOS behave the same as Windows. System.Text.Json and minimal APIs serialise and bind internal records the same way on every OS, and the dashboard and endpoint tests exercise that.

Not done

Refs #198

The dashboard's 29 nested wire records and the classify, /v1/models and
count_tokens bodies become internal: their contract is the JSON. Options
move to the package's own namespace (RouterDashboardOptions to
SemanticRouter.AspNetCore, SqliteStateOptions to
SemanticRouter.Storage.Sqlite), and every SemanticRouterBuilder extension
class is in Microsoft.Extensions.DependencyInjection, as the Onnx and
Copilot ones already were. The breaks against 0.5.0 are suppressed.

docs/architecture.md describes the convention and PublicApiNamespaceTests
enforces it.

Refs #198

Co-Authored-By: Claude <noreply@anthropic.com>
@siebe41 siebe41 added the usage-sprint Opened by the Oct 2026 usage sprint label Oct 20, 2026
Co-Authored-By: Claude <noreply@anthropic.com>
It had landed under 0.3.0's Changed section.

Refs #198

Co-Authored-By: Claude <noreply@anthropic.com>
@siebe41
siebe41 marked this pull request as ready for review October 20, 2026 12:15
@siebe41
siebe41 merged commit 5ae663a into main Oct 20, 2026
15 checks passed
@siebe41
siebe41 deleted the sprint/api/A01-builder-namespace branch October 20, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

usage-sprint Opened by the Oct 2026 usage sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant