Skip to the content.

Task 109 — OpenAPI producer + Scalar UI + Build-Time Generation

Phase: 8 Status: done Owner: — Slug: add-openapi-producer Studio Stage: 0 (pre-req для task_113/115)

Goal

Добавить встроенный в .NET 10 OpenAPI producer (Microsoft.AspNetCore.OpenApi) в Hercules.WebApi:

  1. Runtime: /openapi/v1.json — OpenAPI 3.1 документ при запуске агента
  2. Scalar UI: /scalar — интерактивная документация API (тест endpoints из браузера)
  3. Build-Time: openapi.json генерируется при dotnet build → коммитится в git → виден в PR diff
  4. XML doc comments → OpenAPI descriptions автоматически

Acceptance criteria

Runtime OpenAPI

Scalar UI

Build-Time Generation

XML Documentation Comments

Validation

Implementation details

.csproj changes

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
  <LangVersion>latest</LangVersion>
  <RootNamespace>Hercules.WebApi</RootNamespace>
  <AssemblyName>Hercules.WebApi</AssemblyName>
  <InvariantGlobalization>true</InvariantGlobalization>

  <!-- OpenAPI build-time generation -->
  <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
  <OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions>

  <!-- XML documentation comments → OpenAPI descriptions -->
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="Scalar.AspNetCore" Version="*" />
  <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="*" />
</ItemGroup>

Program.cs changes

// OpenAPI document generation (built-in .NET 10, OpenAPI 3.1)
builder.Services.AddOpenApi();

// Build-time detection: app is launched by GetDocument.Insider during dotnet build
var isBuildTime = Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider";

if (!isBuildTime)
{
    // Services requiring config (DB connection, API keys, OpenTelemetry)
    // builder.AddOpenTelemetry();
    // builder.Services.AddDbContext<HerculesDbContext>(...);
}

var app = builder.Build();

// Runtime OpenAPI document at /openapi/v1.json
app.MapOpenApi();

// Scalar UI at /scalar (interactive API documentation + testing)
app.MapScalarApiReference();

app.Run();

Workflow после task_109

Developer changes API (adds endpoint, DTO, .Produces<T>())
  ↓
dotnet build
  ↓
openapi.json regenerated in Hercules.WebApi/ (build-time)
  ↓
git add openapi.json && git commit
  ↓
PR review: openapi.json diff shows contract changes ← KEY BENEFIT
  ↓
CI: dotnet build (regenerates openapi.json) + dotnet test
CI: npm run gen:api (Studio + Web-UI regen from openapi.json)

Sources for TS codegen

After task_109, TS codegen (task_113/115) can use TWO sources:

  1. Build-time: src/agent/Hercules.WebApi/openapi.json (committed to git, no running agent needed) — для CI
  2. Runtime: http://localhost:8421/openapi/v1.json (running agent) — для dev

Both produce the same document. Build-time is preferred for CI, runtime for dev iteration.

Dependencies

Scope / Likely files

Notes

Acceptance criteria (current state)

Runtime OpenAPI

Build-Time Generation

XML Documentation Comments

Validation

Sub-tasks

Root cause and fix (verified)

The empty build-time document was not an MVC-vs-minimal-API discovery problem in the narrow sense, and it was not caused by the CodeExecutionTool DI ambiguity that the previous revision of this file claimed. Both of those diagnoses were wrong; re-verified against the running agent below.

What was actually wrong

Microsoft.Extensions.ApiDescription.Server’s dotnet-getdocument tool boots the app with the GetDocument.Insider entry assembly and asks the host to emit its registered documents. Running that tool by hand against the real assembly printed:

Generating document named 'v1'.
Using discovered `GenerateAsync` overload with version parameter.
"No action descriptors found. This may indicate an incorrectly configured application..."
Writing document named 'v1' to ...\openapi.json.

and produced a 117-byte skeleton with paths: {}. The same tool against a clean dotnet new web probe project failed outright with Unable to find service type 'Microsoft.Extensions.ApiDescriptions.IDocumentProvider', i.e. the tool’s discovery is not reliable for this host at all.

The fix

Dropped Microsoft.Extensions.ApiDescription.Server and generate the document from the app itself via a custom GenerateOpenApiDocument MSBuild target that launches Hercules.WebApi.dll with the openapi-output flag. Program.cs then:

  1. await app.StartAsync() — required. Without a started host the EndpointDataSource is not finalised and the document serialises with paths: {} even though every MapXxx() call has already run. This was the single decisive step.
  2. Resolves the document provider. AddOpenApi() registers it as a keyed service under the key "v1", so unkeyed GetRequiredService<IOpenApiDocumentProvider>() throws No service for type ... has been registered. GetRequiredKeyedService<...>("v1") is the correct call.
  3. Serialises via document.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1) and writes UTF-8 without BOM (Orval and Spectral both fail to parse a BOM).

Because this goes through the very same IOpenApiDocumentProvider that backs the runtime /openapi/v1.json endpoint, the committed file and the served document cannot drift.

In build mode the host binds an ephemeral loopback port (http://127.0.0.1:0) instead of 8421: StartAsync() opens a real listener, and a build must not fail because a dev server already holds 8421, nor expose the production port while doing so. app.Run() stays behind if (!isBuildTime) so no path can block on a listening socket.

CodeExecutionTool DI ambiguity — resolved, not pre-existing

The previous revision of this file claimed app.Build() threw on constructor ambiguity because CodeExecutionTool had two constructors. That is stale: the type now has a single IServiceProvider constructor (src/agent/Tools/CodeExecutionTool.cs:24) and is registered through an explicit factory (Program.cs:426). The agent starts, serves the runtime document, and serves Scalar. No workaround is needed for it.

Microsoft.OpenApi 2.0.0 vulnerability — resolved

Microsoft.Extensions.ApiDescription.Server 10.0.0 pulled in Microsoft.OpenApi 2.0.0 (NU1903, GHSA-v5pm-xwqc-g5wc). Removing that package drops the warning; the project now resolves Microsoft.OpenApi 2.7.5 via Microsoft.AspNetCore.OpenApi 10.0.11.

Implementation notes

Validation (this revision)

Dependencies

Scope / Likely files

Reference