Update Project Resource v2 plan for file app builds - #19086
Update Project Resource v2 plan for file app builds#19086Damian Edwards (DamianEdwards) wants to merge 2 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7d4b40b3-befc-4ce1-b581-10074f1ee702
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19086Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19086" |
There was a problem hiding this comment.
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
.slnxbuild. - 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 --fileas.slnxinput: 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.csprojfiles. 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`. | |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.SuppressBuildcannot currently suppress the"project"IDE launch build. It only adds--no-buildto process arguments;ProjectLaunchConfigurationFactorydoes not read it, andProjectLaunchConfigurationhas 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
| 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
Adam Ratzman (adamint)
left a comment
There was a problem hiding this comment.
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`; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
Description
The Project Resource v2 plan excluded file-based apps from its coordinated initial build, leaving file apps that share
#:projectdependencies vulnerable to concurrent writes in sharedbinandobjdirectories.This updates the plan to:
.slnx.ProjectReferenceitems withdotnet build <app.cs> --no-restore -getItem:ProjectReference -getResultOutputFile:<path>, adding resolved.csprojFullPathvalues to the initial.slnx.Serialized file-app builds remain the correctness fallback when evaluated-item discovery is not implemented.
Fixes #19037
Checklist
<remarks />and<code />elements on your triple slash comments?