Skip to content

Add .NET support via turbo-managed dotnet tasks #39

@gtbuchanan

Description

@gtbuchanan

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions