911d83a2 feat(products-api): add a .NET API and generate the Product type from it (#486)
* feat(products-api): add a .NET API and generate the Product type from it
`apps/products-api` is an ASP.NET Core minimal API serving the same products
the storefront already had hardcoded. `Microsoft.Extensions.ApiDescription.Server`
writes its OpenAPI document during `dotnet build`, and
`shared-product-types:codegen` turns that document into the TypeScript
`Product` the Angular apps import. Rename a property on the C# record and the
front end stops typechecking.
Notes on the wiring, since most of it is not obvious:
- The document lands in obj/, which @nx/dotnet already infers as a build
output, so no <OpenApiDocumentsDirectory> and no outputs override are
needed. The plugin takes no options at all.
- `implicitDependencies: ["ProductsApi"]` is what connects the two sides. The
generated code is gitignored, so there is no import for Nx to read, and
naming the task in dependsOn instead would leave `nx affected` blind: a C#
change would mark one project affected and the guard would never run.
- codegen hashes the document by exact filename rather than all of obj/, which
MSBuild rewrites on every build.
- The typecheck default adds the generated sources to consumers' inputs.
@nx/js/typescript already covers .d.ts transitively, but this workspace
resolves libraries to their sources, so the .ts has to be hashed too.
- `postinstall` restores the .NET project, because the inferred build runs
--no-restore.
- @hey-api/openapi-ts generates the types: two files, no JVM, and it carries
the OpenAPI descriptions through as JSDoc.
* ci: run the .NET toolchain on the Nx Cloud agents
The default `linux-medium-js` agents have no .NET SDK, so ProductsApi:build
and the codegen that depends on it failed once Nx Cloud distributed them.
`linux-medium-js-dotnet` is that template plus a toolchain: the JS init steps
copied from nrwl/nx-cloud-workflows' linux.yaml, the same install-mise step
its dotnet template uses, and a `dotnet restore` warm-up.
The warm-up is not just for the NuGet cache. The mise shim resolves its config
on the first `dotnet` invocation, and doing that during init keeps the probe
out of a sandboxed task, where it surfaces as an undeclared read of mise.toml.
nrwl/nx has the same step for the same reason.
Also starts one Xvfb per agent and points DISPLAY at it. Cypress only spawns
its own when DISPLAY is unset, and it always picks :99, so two e2e tasks
landing on one agent raced for the display and the loser died with "Display
:99 is already in use". `parallelism: false` is not available here, since Nx
rejects a non-parallel task that depends on a continuous one.
The sandboxing config excludes the reads the Cypress preset's
ciWebServerCommand drags in: it nests an `nx run <app>:serve` inside the e2e
task, so the serve's dependency chain runs in that task's sandbox.
* ci: install the .NET SDK before yarn
`postinstall` runs `dotnet restore`, but node/install-packages ran before the
SDK install step, so yarn failed with `command not found: dotnet` (exit 127).
Move the install above it.
The agents were already fine; this is the main CircleCI job only.
* refactor: pull codegen in with ^codegen instead of naming the task
`^codegen` reaches through projects that have no codegen target of their own,
so one targetDefault covers every app instead of each one naming
`shared-product-types:codegen`.
I previously concluded the opposite. That test was wrong: the app's
project.json declared `dependsOn: ["^build"]` with no `"..."`, which replaces
the targetDefault rather than inheriting it, so the default never applied.
Keeping `"..."` in the project-level list is what makes it work.
Verified from a clean slate: build pulls ProductsApi:build then codegen, both
e2e suites pass with codegen pulled in through serve, and the rename guard
still fires.
* ci: use the current agent image in the dotnet launch template
nrwl/nx-cloud-workflows retired ubuntu22.04-node20.19-v2 on 2026-08-10 in
favour of ubuntu22.04-node24.14-v1, which every upstream template now uses.
This template was copied from the older revision, so its agents had no image
to boot from and the run ended with zero completed tasks.
Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
* ci: drop the postinstall restore in favour of the agent init step
The hook ran 'dotnet restore' on every 'yarn install', so any environment
without a .NET SDK failed at install time rather than when a .NET task ran.
Netlify's deploy preview is one of those. The agents already restore in
'Restore .NET projects', so the hook was a second copy of that work.
Claude-Session: https://claude.ai/code/session_01Sr9WDU54dksQvnDxB6csPZ
* fix(ci): declare mise.toml and tsconfig.json as inputs
Task sandboxing flagged two unexpected reads:
- ProductsApi:build reads mise.toml, because the dotnet shim resolves the
SDK version from it. It belongs in sharedGlobals regardless: a toolchain
bump genuinely changes what every task produces.
- shared-product-types:codegen reads tsconfig.json. Its inputs array
replaced the defaults rather than merging with them, so sharedGlobals
never applied. Add it back explicitly.
The mise.toml entry in the e2e exclusions is now redundant, since every
task declares it through sharedGlobals.
Claude-Session: https://claude.ai/code/session_01FMpiAGxMmARaZoonRd7Cov
* ci: restore the .NET projects before the deploy step
The `deploy` step runs with `--no-agents`, so `ProductsApi:build` executes
on the CircleCI container rather than on an Nx Cloud agent. Dropping the
postinstall restore left that container with a .NET SDK but no restored
packages.
Restoring during setup is not enough. The preceding `affected` step pulls
that task's cached outputs down from the agent that ran it, and `obj` is
one of those outputs, so the agent's `project.assets.json` overwrites the
local one and points at NuGet paths that exist only on the agent. The
restore has to come after that download, or `dotnet build --no-restore`
fails with NETSDK1064.
Claude-Session: https://claude.ai/code/session_01FMpiAGxMmARaZoonRd7Cov
* refactor(types): configure codegen from package.json
Per review on the blog post: for a TypeScript project, a package.json
script is the more natural place for this than a project.json target.
The command moves to `scripts.codegen` and the graph configuration to the
`nx` block, so project.json goes away entirely. Two consequences worth
knowing:
- An inferred script runs with the project directory as its cwd, so the
input path is relative to the library rather than the workspace root,
and {projectRoot} no longer applies to it. The `outputs` entry still
uses the token, since that is Nx config rather than the script.
- Yarn only exposes binaries a workspace declares, so @hey-api/openapi-ts
moves from the root to this library. It is the only project that runs
it, so this is where it belongs.
Claude-Session: https://claude.ai/code/session_01FMpiAGxMmARaZoonRd7Cov
* refactor(types): trim the inferable config
sourceRoot and projectType were carried over from the deleted
project.json, and Nx does not need either of them here.
name stays. Without it the project is called after the package,
@nx-example/shared-product-types, which is out of step with the
short names its siblings use.
Claude-Session: https://claude.ai/code/session_01FMpiAGxMmARaZoonRd7Cov
* fix(types): hash the generator, exclude yarn's own reads
Running codegen as a package.json script puts yarn in the process tree,
and yarn reads .yarnrc.yml, yarn.lock and .yarn/install-state.gz before
running anything. Those are the package manager checking itself, not
inputs to the generator, so they are excluded rather than declared.
install-state.gz especially: it is rewritten on every install and differs
between machines, so hashing it would never settle.
Reading that report turned up a real bug. The task did not hash the
library's package.json, so bumping @hey-api/openapi-ts was a cache hit
and you would keep the types the old generator produced. Verified both
ways: a cache hit before the fix, a re-run after.
Claude-Session: https://claude.ai/code/session_01FMpiAGxMmARaZoonRd7Cov
* feat(products-api): target .NET 10
The blog post that documents this example already showed 10.0.11, which is
what `dotnet add package` resolves today. The example was the thing lagging.
mise.toml drives the SDK on the Nx Cloud agents through install-mise, and
the CircleCI container installs from its own channel flag, so both move
together with the target framework.
Package versions came from `dotnet add package` rather than being written
by hand, and both resolved to 10.0.11.
Claude-Session: https://claude.ai/code/session_01FMpiAGxMmARaZoonRd7Cov