Skip to content

Update Project Resource v2 plan for file app builds - #19086

Open
Damian Edwards (DamianEdwards) wants to merge 2 commits into
mainfrom
damianedwards-update-project-resource-v2-plan
Open

Update Project Resource v2 plan for file app builds#19086
Damian Edwards (DamianEdwards) wants to merge 2 commits into
mainfrom
damianedwards-update-project-resource-v2-plan

Conversation

@DamianEdwards

@DamianEdwards Damian Edwards (DamianEdwards) commented Aug 7, 2026

Copy link
Copy Markdown
Member

Description

The Project Resource v2 plan excluded file-based apps from its coordinated initial build, leaving file apps that share #:project dependencies vulnerable to concurrent writes in shared bin and obj directories.

This updates the plan to:

  • Build project entrypoints together through the generated .slnx.
  • Build file-based entrypoints serially behind the same startup barrier.
  • Suppress launch-time rebuilds in watch, non-watch, and IDE/debug paths.
  • Optionally evaluate file-app ProjectReference items with dotnet build <app.cs> --no-restore -getItem:ProjectReference -getResultOutputFile:<path>, adding resolved .csproj FullPath values to the initial .slnx.
  • Extend the planned tests and playground coverage to include multiple file apps and project resources sharing dependencies.

Serialized file-app builds remain the correctness fallback when evaluated-item discovery is not implemented.

Fixes #19037

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7d4b40b3-befc-4ce1-b581-10074f1ee702
Copilot AI balanced review requested due to automatic review settings August 7, 2026 00:10
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19086

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19086"

@github-actions github-actions Bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the Project Resource v2 plan to coordinate file-based app builds and prevent shared-output races.

Changes:

  • Serializes file-app builds behind the .slnx build.
  • Suppresses launch-time rebuilds.
  • Expands planned regression coverage.
Suppressed comments (2)

docs/plans/project-v2-csharpprogram-watch.md:375

  • Session 5 should not instruct the implementation to consume dotnet reference list --file as .slnx input: its output is human-oriented, includes non-project file references, and preserves unresolved directive text. Remove this optimization from the session or first require a resolved, project-only SDK output contract.
(§5.3): build a temp `.slnx` containing all project entrypoints and, on supported SDKs, the references returned
by `dotnet reference list --file <app.cs>`; then serially `dotnet build` every file-based entrypoint so the SDK
discovers and builds its complete `#:project` graph without overlapping another build. Treat both phases as one
startup barrier **identically for watch and non-watch**; stream logs; fail fast; launch from the result with
build suppression. The reference-list optimization must be SDK-gated and must not replace the serial fallback.

docs/plans/project-v2-csharpprogram-watch.md:214

  • This discovery step cannot reliably produce the set described here. The SDK's GetReferencesForDisplay() emits each directive's original text and mixes project references with file-app references; it does not resolve project directories, relative paths, or MSBuild expressions to .csproj files. Feeding that human-readable output into SolutionPersistence can therefore add invalid entries. Use a resolved project-only contract, or omit this optional phase and retain the serialized fallback.
1. Collect and de-duplicate all project-based `DotnetProjectResource` `.csproj` entrypoints. As an optional
   optimization, when the active SDK is `10.0.400` or `11.0.100-preview.7` and later, run
   `dotnet reference list --file <app.cs>` for each file-based entrypoint and add its discovered `#:project`
   references to the same set. Generate a temp `.slnx` (`Microsoft.VisualStudio.SolutionPersistence`) from
   that set and run one coordinated `dotnet build`. Older SDKs skip discovery and rely on the serialized

| **D5** | **Core exposes run configuration as state** on `DistributedApplicationExecutionContext` (a `RunConfiguration` with a `WatchEnabled` property); language packages query it. **All watch mechanics** (server, `host`/`resource`/`server` commands, pipes, builds) live in the language package. Core is **not** involved in watch details. |
| **D6** | **Watch tool referenced from `Aspire.Hosting.Dotnet`** via a NuGet `PackageReference` (`GeneratePathProperty=true`) + a `.targets` file that injects the tool dll path as **app-host assembly metadata** (the DCP/dashboard/terminal-host pattern); the running app host invokes it with `dotnet exec`. **Not bundled in the CLI.** The CLI obtains the tool for the `host` command by resolving it from the **restored app host project** (handled in the app-host-watch session). |
| **D7** | **Coordinated INITIAL build is in scope**, owned by `Aspire.Hosting.Dotnet`: generate a temp `.slnx` of all `DotnetProjectResource` `.csproj`s and run **one coordinated `dotnet build`** before services start — **identically for watch and non-watch**. The watch tool's `server`/`host`/`resource` perform only **incremental** builds, never the initial one. Library: `Microsoft.VisualStudio.SolutionPersistence`. |
| **D7** | **Coordinated INITIAL build is in scope**, owned by `Aspire.Hosting.Dotnet`, for **both project and file-based entrypoints** ([#19037](https://github.com/microsoft/aspire/issues/19037)). Before any service starts, generate a temp `.slnx` of all `.csproj` entrypoints and run one `dotnet build`, then run `dotnet build <app.cs>` for each file-based entrypoint **serially**. As an optional incremental improvement, when the active SDK supports file apps in `dotnet reference` (.NET SDK `10.0.400` or `11.0.100-preview.7` and later), run `dotnet reference list --file <app.cs>` for each file entrypoint and include the discovered `#:project` references in the initial `.slnx`. The serial file builds remain the correctness fallback for older SDKs and still prevent two file apps, or a file app and the `.slnx` build, from writing a shared dependency's outputs concurrently. Launches consume those outputs without another initial build. This is identical for watch and non-watch; the watch tool performs only later **incremental** builds. Library: `Microsoft.VisualStudio.SolutionPersistence`. |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah interesting! What about using dotnet build app.cs -getItems:ProjectReference instead?

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7d4b40b3-befc-4ce1-b581-10074f1ee702
Copilot AI review requested due to automatic review settings August 7, 2026 00:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

docs/plans/project-v2-csharpprogram-watch.md:228

  • IProjectMetadata.SuppressBuild cannot currently suppress the "project" IDE launch build. It only adds --no-build to process arguments; ProjectLaunchConfigurationFactory does not read it, and ProjectLaunchConfiguration has no no-build field because the IDE owns the build. F5 can therefore rebuild a shared dependency after the barrier, contradicting this PR's launch-time build-suppression guarantee. The plan needs an explicit IDE/DCP contract change (or a different launch path) that actually disables that build.
   installed, mark each resource's `IProjectMetadata.SuppressBuild` as `true` so both plain process launches
   and the `"project"` IDE/debug path consume the coordinated outputs. Update both argument-generation paths

Comment on lines +213 to +214
structured item output, take `FullPath` only for evaluated `.csproj` references, normalize/de-duplicate
those paths into the same set, and reject malformed output rather than guessing from raw directive text.
`WaitForStart(server)`. Coordinated initial build (session 5) runs first; the server owns incremental builds.
watch server with all `DotnetProjectResource` entrypoint paths (`.csproj` and `.cs`), (b) rewrites each `DotnetProjectResource` to
`dotnet exec <tool> resource --entrypoint <entrypoint> --server <pipe> --no-launch-profile -e K=V …`, (c)
`WaitForStart(server)`. The complete coordinated initial-build barrier (session 5) runs first; the server owns

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anything actually start this server? §5.2 adds it with WithExplicitStart, while WaitForStart(server) only waits; it does not start an explicit-start dependency. That leaves the server NotStarted and every service waiting forever. I think the plan should either remove WithExplicitStart and auto-start it after the build barrier, or define the code path that explicitly starts it.

the `.slnx` build, shared project references never have concurrent writers, including references shared by
file apps and traditional project resources.
3. Start no `DotnetProjectResource` until the entire barrier succeeds. Once this always-on coordinator is
installed, mark each resource's `IProjectMetadata.SuppressBuild` as `true` so both plain process launches

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What rebuilds a file app after the initial barrier? AddLifeCycleCommands intentionally gives file apps no Rebuild command or rebuilder because each start currently rebuilds them. Once every later start uses --no-build, an edit followed by Restart just runs stale output (and a cleaned cache will not start at all). Could the plan route Start/Restart through the serialized coordinator, or retain a build path after the initial launch?

non-watch**; stream logs; fail fast; launch from the result with build suppression. Evaluated-item discovery
must not replace the serial fallback.

**Verify:** from a TS app host then a C# app host: (1) multiple `.csproj` resources share a library; (2) two

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add failure and cancellation coverage to this verification? The barrier is the startup gate, but these scenarios only cover successful builds. A failing or cancelled file-app build should prove later builds do not run, the watch server and services never start, and the active process is terminated and cleaned up.

@adamint Adam Ratzman (adamint) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two lifecycle blockers that I think need to be resolved before this plan is ready: the explicit-start watch server has no start path, and always suppressing file-app builds removes rebuild semantics for Start/Restart. I also left a test coverage comment for barrier failure and cancellation. Details inline.

those paths into the same set, and reject malformed output rather than guessing from raw directive text.
The query performs MSBuild evaluation without executing the build, so directory-form and relative
`#:project` references are resolved consistently with the generated file-app project. Generate a temp
`.slnx` (`Microsoft.VisualStudio.SolutionPersistence`) from that set and run one coordinated `dotnet build`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which configuration does this build use? Launches pass the AppHost configuration, so a Release app host would get a Debug .slnx build plus a --no-build --configuration Release launch and no outputs to run. Line 221 already says the file-app phase must use the launch inputs; could we extend that to the .slnx build and the -getItem query, and name the single configuration source?

one at a time in deterministic resource order and with the same configuration/build-affecting inputs used
for launch. Do not hand-parse `#:project`; the .NET SDK's generated file-app project is the authoritative
evaluator for directive syntax and project-reference discovery. Because these builds are serialized after
the `.slnx` build, shared project references never have concurrent writers, including references shared by

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says shared references never have concurrent writers, but the barrier only collects DotnetProjectResource. Path-based AddProject(name, path) and AddCSharpApp stay unchanged under D2 and still build at launch, so a v1 file app sharing a #:project library with a v2 one can still race. Could we scope this guarantee to the DotnetProjectResource set and call mixed v1/v2 graphs a known limitation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Coordinate Project Resource v2 builds for file-based apps with shared project references

3 participants