Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions AuthKit.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
</Folder>
<Folder Name="/Plugins/Solutions/">
<Project Path="src/Plugins/Solutions/DevTokens/DevTokens.csproj" />
<Project Path="src/Plugins/Solutions/DevTools/DevTools.csproj" />
</Folder>
<Folder Name="/Tools/">
<Project Path="tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj" />
</Folder>
<Project Path="src/Core/Core.csproj" />
<Project Path="src/Host/Host.csproj" />
<Folder Name="/Tests/">
<Project Path="tests/Host/AuthKit.Host.Tests.csproj" />
<Project Path="tests/Plugins/Abstractions/AuthKit.Plugins.Abstractions.Tests.csproj" />
</Folder>
</Solution>
4 changes: 0 additions & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<PackageVersion Include="Grpc.Net.Client" Version="2.71.0" />
<PackageVersion Include="Grpc.Tools" Version="2.72.0" />
<PackageVersion Include="Marten" Version="8.13.2" />

<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
Expand All @@ -24,14 +23,11 @@
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />

<PackageVersion Include="Scrutor" Version="6.1.0" />

<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.Swagger" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.2.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />

<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />

<PackageVersion Include="WolverineFx" Version="5.0.0" />
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ COPY ["src/Host/Host.csproj", "src/Host/"]
COPY ["src/Core/Core.csproj", "src/Core/"]
COPY ["src/Plugins/Abstractions/AuthKit.Plugins.Abstractions.csproj", "src/Plugins/Abstractions/"]
COPY ["src/Plugins/Solutions/DevTokens/DevTokens.csproj", "src/Plugins/Solutions/DevTokens/"]
COPY ["src/Plugins/Solutions/DevTools/DevTools.csproj", "src/Plugins/Solutions/DevTools/"]

COPY ["tests/Host/AuthKit.Host.Tests.csproj", "tests/Host/"]
COPY ["tests/Plugins/Abstractions/AuthKit.Plugins.Abstractions.Tests.csproj", "tests/Plugins/Abstractions/"]
COPY ["tools/PluginContractValidator/PluginContractValidator.csproj", "tools/PluginContractValidator/"]

RUN dotnet restore "AuthKit.slnx"
Expand All @@ -30,6 +34,7 @@ FROM build AS publish
WORKDIR /src
RUN dotnet publish "src/Host/Host.csproj" -c Release -o /app/publish
RUN dotnet publish "src/Plugins/Solutions/DevTokens/DevTokens.csproj" -c Release -o /app/publish/plugins/DevTokens
RUN dotnet publish "src/Plugins/Solutions/DevTools/DevTools.csproj" -c Release -o /app/publish/plugins/DevTools
COPY src/Plugins/Solutions/DevTokens/plugin.manifest.json /app/publish/plugins/DevTokens/plugin.manifest.json

FROM base AS final
Expand Down
57 changes: 57 additions & 0 deletions Docs/ADR/020-devtools-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./019-plugin-metadata-attribute.md) | [Next](./021-swagger-serving-via-reflection.md)

# [ADR-020] Host Developer Tools Through A Dedicated DevTools Plugin

*2026-09* | Status: accepted

**Tag:** #adr_020

**Date:** 2026-09-11

**Scope:** Host + Plugins.Solutions.DevTools

## Context

AuthKit needs developer facing tooling to exercise its own surfaces: an interactive page to call any exposed gRPC method, and Swagger UI to browse the REST/OpenAPI surface. Earlier work exposed a gRPC UI as a standalone `GrpcUI` plugin and served Swagger UI on the host itself through `UseSwagger` / `UseSwaggerUI` (Swashbuckle direct middleware). The two tools lived in different architectural homes: one was a plugin, the other was hard wired into the host's request pipeline.

## Problem

A tool baked into the host can only ever be enabled together with the host, couples the host to Swashbuckle's serving middleware, and cannot be shared across host flavors or removed without editing host code. A dedicated `GrpcUI` plugin covered gRPC but left the REST/OpenAPI tooling outside the plugin boundary two mechanisms for the same job, two places to configure. Tooling URLs were also fixed implicit host paths rather than configurable plugin owned prefixes.

## Decision

Both developer tools move into single `DevTools` plugin (`src/Plugins/Solutions/DevTools`), replacing the standalone `GrpcUI` plugin and removing Swagger serving from the host:

- The plugin owns its URL space through `DevToolsOptions`: gRPC UI at `GrpcUiPathBase` (`/grpc-ui`), Swagger UI under the `Swagger` section (default route prefix `swagger`, document name `v1`, title `AuthKit API`), and a landing page at `PathBase` (`/devtools`).
- The gRPC UI is in process: `GrpcServiceCatalog` scans the default assembly load context for static `ServiceDescriptor` properties, `GrpcDynamicInvoker` executes unary methods dynamically over `Grpc.Net.Client` without generated stubs, and `DevToolsMiddleware` dispatches `/grpc-ui` and its `/api/services` + `/api/invoke` endpoints.
- Swagger document generation stays in the host (`RestfulConfiguration` + `AddSwaggerGen`); only the SWAGGER SERVING middleware moves into `DevTools` via `SwaggerHost`.
- The host no longer calls `UseSwagger` / `UseSwaggerUI` `AppMiddlewareConfiguration` contains only the plugin slot. Developer tools are present in a deployment exactly when the plugin is deployed.
- Swagger UI serving is gated: by default enabled only in the development environment (`Swagger.Enabled ?? environment.IsDevelopment()`), with pinning of the OpenAPI spec version for the served document.
- The host is published with `DevTools` selected by default (solution file and Dockerfile), so a default deployment keeps both tools available.

### Design Rationale

- **One tooling surface**: REST (Swagger) and gRPC (in-process UI) are both interactive representations of the same host one plugin owns the developer experience.
- **Consistent plugin boundary**: everything a template developer visits is contributed by plugin, under plugin owned path bases, removable by not deploying the plugin.
- **In-process over proxy**: the gRPC UI talks to the host directly rather than fronting separate process and using gRPC reflection, keeping discovery local and configuration minimal.
- **No stubs needed**: dynamic invocation over method descriptors means the plugin never needs generated client code for the services it renders.

## Rejected

- Keeping Swagger serving in the host (`UseSwagger`/`UseSwaggerUI`) couples host to specific tool and a specific serving middleware.
- A standalone `GrpcUI` plugin with host owned Swagger two homes for equivalent functionality.
- A gRPC proxy process (eg. maintained third party gRPC UI binary) for streaming and health support additional deployment process and transport hop the extra support was deemed unnecessary for an internal developer tool.
- Serving Swagger from the plugin while re-emitting document generation there would duplicate `AddSwaggerGen` configuration generation stays in the host, serving stays in the plugin.

## Consequences

The host's `AppMiddlewareConfiguration` is smaller and no longer references Swashbuckle. The `DevTools` solution owns compiler friendly tooling code (catalog, invoker, middleware, options, and UI assets). Swagger UI availability depends on plugin deployment and environment gating rather than host build configuration. The gRPC UI only supports unary methods streaming methods are reported as unsupported rather than approximated. Configuration is centralized in `DevToolsOptions`, resolved with environment variables (`GRPC_UI_TARGET`, `DEV_CERT_PORT_GRPC`) for local development against the HTTPS gRPC endpoint.

## Related

- [ADR-009](./009-dynamic-plugin-discovery.md) - plugin contract and dynamic discovery
- [ADR-010](./010-plugin-loading-from-directory.md) - plugin middleware slot used by DevToolsMiddleware
- [ADR-013](./013-dual-rest-and-grpc-transport.md) - dual REST + gRPC surfaces being exercised
- [ADR-021](./021-swagger-serving-via-reflection.md) - how SwaggerHost serves SwaggerUI through reflection

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./019-plugin-metadata-attribute.md) | [Next](./021-swagger-serving-via-reflection.md)
55 changes: 55 additions & 0 deletions Docs/ADR/021-swagger-serving-via-reflection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./020-devtools-plugin.md) | [Next]()

# [ADR-021] Serve Swagger Through Reflection-Based SwaggerHost In DevTools

*2026-09* | Status: accepted

**Tag:** #adr_021

**Date:** 2026-09-11

**Scope:** Plugins.Solutions.DevTools

## Context

ADR-020 moved Swagger UI serving from the host into the `DevTools` plugin. The plugin already consumed the OpenAPI document Provider (`ISwaggerProvider`, registered by `RestfulConfiguration` + `AddSwaggerGen`), but the actual document serialization and HTML UI rendering were performed by Swashbuckle's `SwaggerMiddleware` and `SwaggerUIMiddleware` — both of which are **internal** to their NuGet packages, so a plugin cannot call them the way the host used to.

## Problem

The plugin must reproduce what the host previously did with the public `UseSwagger` / `UseSwaggerUI` extension methods, but the middlewares behind those extensions are not part of the public API surface. The plugin also needs to control the OpenAPI spec version pinned for the served document — independently of any document that `RestfulConfiguration` might generate for other consumers.

## Decision

`DevTools` serves Swagger with a reflection-based `SwaggerHost`:

- `SwaggerHost` resolves the internal `Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware` and `Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware` types by assembly-qualified name at startup, constructs one instance per middleware via `Activator.CreateInstance` with the standard (notFound, options) constructor shape, and invokes the internal `Invoke` methods reflectively per request.
- The Swagger document middleware is configured with a route template under the configured Swagger route prefix and the resolved OpenAPI spec version.
- The Swagger UI middleware receives UI options (route prefix, document title, `IndexStream` pointing at the embedded `index.html` resource) and an endpoint pointing at the document route.
- `DevToolsMiddleware` decides whether a request path belongs to the plugin (gRPC UI, Swagger, landing page) and, for the Swagger segment, delegates to `SwaggerHost.TryServeAsync`; a 404-not-found `RequestDelegate` is handed to the Swashbuckle middlewares when the path does not match.
- The OpenAPI spec version for the served document is resolved by `SwaggerHost.ResolveSpecVersion`: a `DevTools:Swagger:SpecVersion` override, then the host's `OpenApi:SpecVersion`, defaulting to `3.0`. Supported values are `3.0.x` and `3.1.x`; an unknown value logs a warning and falls back to `3.0`.
- Because the served document uses the spec version pinned for DevTools, mutual-TLS schemes (ADR-018) surface natively (`mutualTLS`) only when the document is pinned to OpenAPI 3.1.

### Design Rationale

- **Reuse over reimplementation**: the plugin does not re-implement OpenAPI serialization or the HTML UI — it drives the exact middleware the host used, through reflection, because the types are internal.
- **Single construction**: middleware instances are built once at plugin startup and reused for every request; per-request work is a reflection invoke, not a rebuild.
- **Independent pinning**: DevTools controls its own document spec version rather than inheriting a fixed host document, which is what lets the OpenAPI mapper's 3.1-dependent cases (ADR-018) work in DevTools.

## Rejected

- Re-implementing the Swagger UI page and OpenAPI serialization inside the plugin — duplicates a large, maintained surface for no architectural gain.
- Depending on the internal Swashbuckle types as a hard library reference — they are internal by design; a hard reference simply would not compile.
- Copying/shipping the Swashbuckle middleware source into the plugin — license and maintenance burden, and drift from upstream bug fixes.
- Removing Swagger serving from DevTools entirely (document-only JSON at a well-known URL) — loses the interactive UI that motivated the plugin.

## Consequences

`DevTools` stays on the Swashbuckle version that ships the internal middlewares; upgrading a Swashbuckle version could change the internal type/methods signatures and break `SwaggerHost` at startup (fail-fast with a descriptive exception). The reflection layer is a deliberate, visible cost: two `Type`/`MethodInfo` lookups and a per-request `Invoke` instead of direct calls. Unknown spec versions degrade to a warning + 3.0 in DevTools serving (so the UI still works), which is intentionally lazier than `RestfulConfiguration`, which hard-fails on unknown spec versions in app configuration.

## Related

- [ADR-013](./013-dual-rest-and-grpc-transport.md) - the OpenAPI surface served by DevTools
- [ADR-020](./020-devtools-plugin.md) - why Swagger serving lives in the plugin at all
- [ADR-018](./018-security-scheme-contract-explicit-handling.md) - mutual-TLS schemes require the 3.1 pin DevTools provides

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./020-devtools-plugin.md) | [Next]()
4 changes: 0 additions & 4 deletions src/Host/Configuration/RestfulConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using AuthKit.Plugins.Abstractions;
using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
using Host.Plugins;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi;

namespace Host.Configuration;
Expand Down
Loading
Loading