feat(repo): add a .NET example for @nx/dotnet - #36701
Conversation
A standalone example workspace showing full-stack type safety across the C#/TypeScript boundary, dogfooding @nx/dotnet from this repo via link: dependencies like examples/react/basic does. The pipeline: - Api:build writes apps/Api/openapi/Api.json during dotnet build, via <OpenApiDocumentsDirectory> plus Microsoft.Extensions.ApiDescription.Server, so there is no separate extraction step - Api:codegen runs openapi-generator-cli over that document - api-client:build compiles the generated client plus a checked-in assert-types.ts that pins the contract - web:build compiles a small consumer; that edge comes from an ordinary package.json dependency, not from Nx config Two declarations worth reading: - nx.json adds the document directory to build's outputs with a "..." spread, so it rides the build cache instead of going missing on a cache hit - codegen and api-client:build hash with dependentTasksOutputFiles, because the document is gitignored and a path input would match nothing and cache forever Targets net9.0 to match the SDK pinned in mise.toml.
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
View your CI Pipeline Execution ↗ for commit 5f2ee1c
☁️ Nx Cloud last updated this comment at |
|
The blog post walking through this example is up as a draft at nrwl/nx-blog#65. The post links back to this directory, so that link stays broken until this lands. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
…a CVE Socket flagged GHSA-w7jw-789q-3m8p (critical) on shell-quote 1.8.3, pulled in transitively through @openapitools/openapi-generator-cli 2.24.0 -> concurrently. 2.24.0 was an arbitrary pin on my part; 2.40.1 was already current. It widens its concurrently range, which resolves shell-quote to 1.9.0, above the 1.8.4 patched version. The Java generator itself stays pinned at 7.16.0 in openapitools.json, so the generated client is unchanged. Re-verified after the bump: nx build web green across all five tasks, breaking the C# record still fails api-client:build with TS2322, restoring it goes back to green, and pnpm audit reports no known vulnerabilities.
|
Fixed in 2bc7510.
The Java generator stays pinned at 7.16.0 in Re-verified after the bump rather than assuming:
No |
…ame clash CI failed on nx sync:check and nx format:check. sync:check was the real one. The example's apps/web/project.json declared a project named "web", which collides with packages/web in this repo, and the duplicate name made both workspace plugins fail to create project configurations. The error surfaced as an AggregateError naming copy-assets-plugin and examples-plugin, with the underlying cause not shown, so it read like an unrelated plugin problem. Renamed the project and its directory to "storefront", which is unique. Verified: without the example, sync:check passed; with it, it failed; after the rename, it passes again. format:check wanted prettier over the README and the two new project.json files. Ran nx format:write. The example still behaves the same: nx build storefront runs all five tasks and the chain is unchanged at storefront:build -> api-client:build -> Api:codegen -> Api:build -> Api:restore.
main-linux failed with: Sandbox violation detected for task api-client:oxlint: 1 unexpected read(s). Root cause is structural rather than that one task. Every other example in this repo is a single root-level project: nx.json and package.json at its root and nothing nested. This one has interior project.json files so it can demonstrate a cross-project graph edge, and those interior projects land in the ROOT graph, where root plugins attach targets to them and the root affected sweep runs them. They cannot work there, because this workspace's node_modules only exists after its own pnpm install. Two changes: - nx.json excludes examples/**/* from @nx/dotnet and @nx/oxlint, matching what @nx/js/typescript and @nx/playwright/plugin already do. Without it the root graph gave the example's csproj a full set of dotnet targets and gave the TS projects an oxlint target. - The example's interior targets are named `compile` rather than `build`. The root CI sweep covers lint, oxlint, test, build, e2e, run and others; `compile` is in none of them. Verified beforehand that `nx run api-client:build` from the repo root does fail, so this was a live problem and not a theoretical one. Verified after: root format:check and sync:check pass, no interior project exposes a swept target name, and inside the example `nx compile storefront` still runs the full chain storefront:compile -> api-client:compile -> Api:codegen -> Api:build -> Api:restore.
|
Pushed The failure was Every other example here is a single root-level project — Two changes:
Verified locally: Worth pushing back on if you'd rather. The alternatives I considered and rejected: |
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
Replaces the hand-written `compile` targets with the `typecheck` target
@nx/js/typescript infers from each tsconfig. A single target default,
`typecheck: { dependsOn: ["^typecheck"] }`, is enough to make it walk the graph,
so the TypeScript side now needs no per-project build config at all.
Two things this surfaced, both worth reading:
- libs/api-client declares `implicitDependencies: ["Api"]`. The generated client
is gitignored, so there is no import for Nx to derive an edge from. Without
the declaration, editing a C# record left both TypeScript projects looking
unaffected.
- Its typecheck also needs `dependentTasksOutputFiles` on `inputs`, for the same
reason codegen does. This was observed rather than predicted: with only
dependsOn wired, breaking the C# record left the pipeline green, because the
gitignored generated client never entered the task hash and the typecheck
replayed a stale cache entry.
tsconfig.base.json uses `declaration` rather than `composite`. Both satisfy
`tsc --build --emitDeclarationOnly`, but composite makes the ROOT workspace's
typescript-sync add project references into the repo's own tsconfig.json,
pointing at an example that cannot build outside its own workspace. The example
also disables @nx/js:typescript-sync for itself, so the two workspaces' sync
generators do not fight over the same files.
Verified: root format:check and sync:check pass, root tsconfig.json is
untouched, no interior project exposes a CI-swept target name, `pnpm validate`
passes, and breaking then restoring the C# record is caught and cleared
deterministically across repeated cycles with caching enabled.
CI failed with:
Sandbox violation detected for task
examples-dotnet-openapi-typescript:validate-example:
12 unexpected read(s), 60 unexpected write(s).
The examples plugin declared `{projectRoot}/dist` and `{projectRoot}/*/dist`,
which describes a flat example. examples/react/basic writes ./dist and fits;
examples/dotnet/openapi-typescript has apps/ and libs/, so everything it
produces lands two levels down (apps/storefront/dist, libs/api-client/dist,
libs/api-client/src/generated) plus MSBuild's apps/Api/{bin,obj} and the
generated OpenAPI document. None of that matched, and task sandboxing rejects
writes the outputs do not cover.
Widened to the union at any depth: dist, bin, obj, openapi, generated and
tsbuildinfo files. Verified both the flat and the nested example resolve the
same list, format:check and sync:check pass, and workspace-plugin still builds.
|
Pushed CI failed with:
Widened the glob to the union at any depth: Worth saying plainly: this is the fourth time this example has collided with tooling that assumes a flat, single-project example.
Each was a real bug and each fix was small, but the pattern is consistent, and it has now cost two changes to shared config ( |
Second half of the validate-example sandbox violation; the writes were covered
in the previous commit. Diagnosed from the actual Nx Cloud sandbox reports
rather than guessed:
npx nx-cloud get sandbox-reports --branch 36701 --since 1d
npx nx-cloud validate sandbox-violations <index.json> --json
Ten of the twelve unexpected reads were the example's OWN source files
(apps/Api/Program.cs, libs/api-client/src/index.ts, and so on). `default` is
{projectRoot}/**/* and Nx assigns each file to the project that owns it, so an
example whose interior directories are themselves projects in this graph loses
its own sources from that glob. Addressing the tree literally with
{workspaceRoot}/<exampleRoot>/**/* covers them regardless of ownership, and
resolves per example (examples/react/basic gets its own path).
The other two were .editorconfig and Directory.Build.props, read from the repo
root because MSBuild and Roslyn walk ancestor directories up from the project
file. Declared those explicitly, alongside tsconfig.base.json, which the root
tsconfig.json extends and which the existing tsconfig.json input never covered.
The remaining MSBuild and NuGet ancestor files are listed whether or not they
exist today, so adding one later does not quietly start tripping sandboxing.
After the change the task validates clean: 12 reads reconciled, 0 still
unexpected, 0 writes. format:check, sync:check and workspace-plugin:build pass.
Reverts both changes to examples-plugin.ts and puts the same declarations in examples/dotnet/openapi-typescript/project.json instead, using the "..." spread so the plugin's inferred inputs and outputs are kept and appended to. The plugin edits were too broad. Every one of the reads they covered belongs to this example: the sandbox report attributed .editorconfig and Directory.Build.props to this task alone, and they are read because MSBuild and Roslyn walk ancestor directories from a .csproj. No other example is a .NET workspace, so declaring MSBuild and NuGet ancestor files for all of them was noise. Same for the deeper outputs globs, which exist only because this example has apps/ and libs/ while the others are flat. The plugin's own comment warns against a project.json in an example, on the grounds that the example's workspace also reads it and implicit dependencies on projects that exist only in this graph would fail validation there. That applies to implicit dependencies; this file declares inputs and outputs only. Verified the inner workspace still resolves its projects and `pnpm validate` still passes with the file present. Validated against the real report rather than by inspection: the task reports ok: true, 12 reads reconciled, 0 still unexpected. Root format:check and sync:check pass, and tools/workspace-plugin is byte-identical to master again. This leaves nx.json as the only file this PR changes outside examples/dotnet.
…lf-Healing CI Rerun]
There was a problem hiding this comment.
Nx Cloud has identified a flaky task in your failed CI:
🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.
🎓 Learn more about Self-Healing CI on nx.dev
The full-stack OpenAPI story is moving to nrwl/nx-examples, where it can sit next to a real front end. What belongs here is a small example of the plugin itself, so this replaces examples/dotnet/openapi-typescript with examples/dotnet/basic. Three projects, no project.json anywhere: apps/Api minimal web API libs/Catalog class library the API references tests/Catalog.Tests xunit project covering the library Everything is inferred from the .csproj files. Verified rather than described: Api gets run and publish, Catalog gets pack, Catalog.Tests gets test, and <ProjectReference> becomes a graph edge, so nx build Api builds Catalog first. nx.json adds "dependsOn": ["...", "restore"] to build, because the inferred build runs dotnet build --no-restore and pnpm validate has to work from a clean checkout. Dropping the TypeScript half removes most of what the previous shape needed: - No interior project.json or package.json, so nothing from the example lands in this repo's graph, and the sandbox inputs and outputs it needed are gone. - The @nx/oxlint exclude is gone too; with no TypeScript in the example there is nothing for it to attach to. Confirmed by removing it and checking. - The @nx/dotnet exclude stays. Without it the root graph infers targets for the example's .csproj files, including run, which the affected sweep runs. That leaves one added line in nx.json as this PR's only change outside examples/, down from three shared files.
`validate-example` failed sandboxing with 2 unexpected reads and 161 unexpected writes. The reads: MSBuild and Roslyn discover `Directory.Build.props` and `.editorconfig` by walking parent directories, and neither stops at a workspace boundary, so the example was climbing out to the Nx repo root and picking up both. That is a sandbox violation, but more to the point it means the example did not build the way a copy of it would for a user. Give it its own `Directory.Build.props` (carrying the same `UseSharedCompilation=false` the root file was after) and an `.editorconfig` with `root = true`, so the walk terminates inside the example. The writes: `dotnet build`/`test` fill bin/ and obj/ next to each .csproj, 164 files locally. Nothing downstream consumes them and obj/ embeds absolute NuGet paths, so they are excluded in the sandboxing config rather than declared as outputs of the wrapper. Scoped to this project and target. Verified by adding a sentinel property to the root Directory.Build.props and confirming the example no longer sees it, while UseSharedCompilation stays false; all three projects build with 0 warnings and the test passes.
The writes exclusion landed, but `validate-example` then failed with 131 unexpected reads, all of them the same bin/ and obj/ files the task had just written: restore writes obj/project.assets.json for the build to consume, and the test run loads its assemblies out of bin/. They are this task's own scratch in both directions, so the exclusion applies to reads as well. Anchored so the two lists cannot drift apart. Confirmed from the sandbox report: 87 reads under tests/Catalog.Tests/bin, 16 under tests/Catalog.Tests/obj, 14 under libs/Catalog/obj, 11 under apps/Api/obj and 3 under libs/Catalog/bin. Nothing outside bin/ or obj/.
What
examples/dotnet/basic— a standalone Nx + pnpm workspace that dogfoods@nx/dotnetfrom this repo throughlink:dependencies, the same wayexamples/react/basicdoes.Three projects, and no
project.jsonanywhere:Registration is automatic: the nested
nx.jsonis whattools/workspace-plugin/src/plugins/examples-plugin.tsglobs for.What it demonstrates
Everything comes from the
.csprojfiles. Verified against the graph rather than described:build,build:release,restore,clean,watchtestCatalog.Testsonlyrun,publishApionlypackCatalogonly<ProjectReference>becomes a graph edge, sonx build ApibuildsCatalogfirst — ordering falls out of the project files rather than out of Nx config.nx.jsonadds"dependsOn": ["...", "restore"]tobuild. The inferredbuildrunsdotnet build --no-restore, andpnpm validatehas to work from a clean checkout. The"..."keeps the inferred^buildand puts the restore ahead of it.One line outside
examples/{ "plugin": "@nx/dotnet", "exclude": ["examples/**/*"], ... }Without it the root graph infers targets for the example's
.csprojfiles, includingrun, which the affected sweep executes — against a workspace that has not been installed. This matches what@nx/js/typescriptand@nx/playwright/pluginalready do forexamples/**.Earlier revisions of this PR also touched
examples-plugin.tsand added an@nx/oxlintexclude. Both are gone: with no interiorproject.jsonorpackage.json, nothing from the example enters this repo's graph, so the sandbox inputs and outputs it needed no longer apply. Confirmed by removing each and re-checking.Verification
pnpm validategreen from a clean checkout, xunit test passesformat:checkandsync:checkpassNotes
net9.0to match the SDK pinned inmise.toml.openapi-typescriptfrom the original scope. Renaming it would detach this PR, so it stays as is.