Summary
Add .NET support to the tooling monorepo by wrapping dotnet commands in turbo-managed tasks, giving .NET projects the same remote caching, task orchestration, and unified CLI experience as JS/TS packages.
Package structure
.csproj packages live under packages/*/ alongside JS packages
turbo:init auto-discovers .csproj files and generates package.json wrappers (workspace identity only)
.csproj name matches PackageId, consumer-created
package.json name derived from directory name (@gtbuchanan/my-package)
- Test project in
test/ (one .csproj per package, categories for fast/slow filtering)
- E2E test project in
e2e/ (artifact tests against packed NuGet)
Example layout:
packages/my-package/
package.json # generated by turbo:init
src/GtBuchanan.MyPackage/
GtBuchanan.MyPackage.csproj
test/GtBuchanan.MyPackage.Test/
GtBuchanan.MyPackage.Test.csproj
e2e/GtBuchanan.MyPackage.E2E/
GtBuchanan.MyPackage.E2E.csproj
Turbo integration
turbo:init discovers .csproj files under workspace globs, generates package.json + scripts
turbo:init scans <ProjectReference> entries in .csproj files and adds corresponding workspace:* dependencies to the generated package.json, so turbo infers dependsOn automatically
turbo:init generates/updates .slnx at repo root mirroring directory structure for IDE support
dotnet restore runs before turbo (like pnpm install)
- Cross-ecosystem dependencies (e.g., .NET app consuming TS client output) work via
workspace:* in package.json
Cache inputs
globalDependencies: Directory.Build.props, Directory.Build.targets, global.json, root .editorconfig
- Per-task inputs:
.csproj, packages.lock.json, source files, package-level .editorconfig
- Intermediate
.editorconfig files (e.g., packages/.editorconfig) must be added to globalDependencies manually (document this)
- .NET tasks include
env: ["CI"] so turbo hashes build configuration into the cache key
Generated config
Directory.Build.props
RestorePackagesWithLockFile enabled
- Per-test-project MTP runner property detected from framework reference:
- MSTest →
EnableMSTestRunner
- NUnit →
EnableNUnitRunner
- xUnit →
UseMicrosoftTestingPlatformRunner
global.json
test.runner set to Microsoft.Testing.Platform
.slnx
- Generated at repo root, mirrors
packages/*/ directory structure
CLI commands
| Command |
Underlying |
Notes |
compile:dotnet |
dotnet build |
--configuration Release when CI=true |
lint:dotnet-format |
dotnet format --verify-no-changes |
Report output for lint regression check (#38) |
test:dotnet |
dotnet test --no-build |
fast + slow |
test:dotnet:fast |
dotnet test --no-build --filter "TestCategory!=Slow" |
Framework-detected filter syntax |
test:dotnet:slow |
dotnet test --no-build --filter "TestCategory=Slow" |
Framework-detected filter syntax |
test:dotnet:e2e |
dotnet test --no-build (e2e project) |
Against packed artifacts |
pack:nuget |
dotnet pack --no-build |
|
--no-build on test/pack commands because turbo already runs compile:dotnet as a dependency
compile:dotnet passes --configuration Release when CI environment variable is set, Debug otherwise
- Commands abstract filter syntax per test framework (MSTest/NUnit/xUnit)
- Consumers can override any script in
package.json for unsupported frameworks or custom needs
turbo:init preserves existing script values (no --force needed to keep overrides)
- Multitargeting (
<TargetFrameworks>) works transparently — dotnet build/test handle all targets, turbo caches the combined output
Design decisions
- Standardize on Microsoft.Testing.Platform (MTP) — VSTest is legacy
- MTP runner enable property added per-test-project
.csproj (not Directory.Build.props) to support mixed-framework migration paths
Directory.Packages.props is NOT a globalDependency — per-project packages.lock.json already captures its effects
- NuGet
PackageId is consumer-controlled in .csproj, not derived from directory name
- Cross-package .NET references managed via
<ProjectReference> in .csproj; turbo:init syncs these to package.json workspace:* dependencies automatically
- RID-specific builds (
dotnet publish) deferred to a future issue — pack:nuget output is platform-agnostic
Summary
Add .NET support to the tooling monorepo by wrapping
dotnetcommands in turbo-managed tasks, giving .NET projects the same remote caching, task orchestration, and unified CLI experience as JS/TS packages.Package structure
.csprojpackages live underpackages/*/alongside JS packagesturbo:initauto-discovers.csprojfiles and generatespackage.jsonwrappers (workspace identity only).csprojname matchesPackageId, consumer-createdpackage.jsonname derived from directory name (@gtbuchanan/my-package)test/(one.csprojper package, categories for fast/slow filtering)e2e/(artifact tests against packed NuGet)Example layout:
Turbo integration
turbo:initdiscovers.csprojfiles under workspace globs, generatespackage.json+ scriptsturbo:initscans<ProjectReference>entries in.csprojfiles and adds correspondingworkspace:*dependencies to the generatedpackage.json, so turbo infersdependsOnautomaticallyturbo:initgenerates/updates.slnxat repo root mirroring directory structure for IDE supportdotnet restoreruns before turbo (likepnpm install)workspace:*inpackage.jsonCache inputs
globalDependencies:Directory.Build.props,Directory.Build.targets,global.json, root.editorconfig.csproj,packages.lock.json, source files, package-level.editorconfig.editorconfigfiles (e.g.,packages/.editorconfig) must be added toglobalDependenciesmanually (document this)env: ["CI"]so turbo hashes build configuration into the cache keyGenerated config
Directory.Build.propsRestorePackagesWithLockFileenabledEnableMSTestRunnerEnableNUnitRunnerUseMicrosoftTestingPlatformRunnerglobal.jsontest.runnerset toMicrosoft.Testing.Platform.slnxpackages/*/directory structureCLI commands
compile:dotnetdotnet build--configuration ReleasewhenCI=truelint:dotnet-formatdotnet format --verify-no-changestest:dotnetdotnet test --no-buildtest:dotnet:fastdotnet test --no-build --filter "TestCategory!=Slow"test:dotnet:slowdotnet test --no-build --filter "TestCategory=Slow"test:dotnet:e2edotnet test --no-build(e2e project)pack:nugetdotnet pack --no-build--no-buildon test/pack commands because turbo already runscompile:dotnetas a dependencycompile:dotnetpasses--configuration ReleasewhenCIenvironment variable is set,Debugotherwisepackage.jsonfor unsupported frameworks or custom needsturbo:initpreserves existing script values (no--forceneeded to keep overrides)<TargetFrameworks>) works transparently —dotnet build/testhandle all targets, turbo caches the combined outputDesign decisions
.csproj(notDirectory.Build.props) to support mixed-framework migration pathsDirectory.Packages.propsis NOT aglobalDependency— per-projectpackages.lock.jsonalready captures its effectsPackageIdis consumer-controlled in.csproj, not derived from directory name<ProjectReference>in.csproj;turbo:initsyncs these topackage.jsonworkspace:*dependencies automaticallydotnet publish) deferred to a future issue —pack:nugetoutput is platform-agnostic