Add experimental CLI-managed C# AppHost mode with shared closure cache#18101
Add experimental CLI-managed C# AppHost mode with shared closure cache#18101danegsta wants to merge 18 commits into
Conversation
Consolidates the MSBuild closure contract (file-name constants, project-file XML emission, and post-build closure reader) shared by PrebuiltAppHostServer (polyglot) and IntegrationClosureRestorer (CLI-managed C# AppHost) into a single IntegrationClosureBuilder static helper so the two consumers cannot drift on the contract. ClosureFileMissingBehavior bridges divergent error semantics: the polyglot path treats missing closure files as a hard error (Throw); the CLI-managed path warns and returns null so RestoreCommand surfaces the failure with its own exit code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ost-design # Conflicts: # src/Aspire.Cli/Commands/RestoreCommand.cs # src/Aspire.Cli/DotNet/DotNetCliRunner.cs # src/Aspire.Cli/Projects/DotNetAppHostProject.cs # tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18101Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 18101" |
There was a problem hiding this comment.
Pull request overview
This PR adds an experimental CLI-managed C# AppHost mode behind the CSharpCliManagedAppHostEnabled feature flag. It mirrors the polyglot AppHost pattern: a thin single-file apphost.cs (using #:project .aspire/modules/Aspire.csproj instead of #:sdk Aspire.AppHost.Sdk) declares integration packages in aspire.config.json, and the Aspire CLI synthesizes, restores, and builds a generated module project under .aspire/modules/. A shared IntegrationClosureBuilder is extracted so both the polyglot PrebuiltAppHostServer and the new IntegrationClosureRestorer use the same MSBuild closure contract (file names, XML emission, and post-build reading).
Changes:
- Extracts shared closure logic into
IntegrationClosureBuilderand shared project-reference resolution intoCSharpIntegrationProjectReferences, refactoringPrebuiltAppHostServerandDotNetBasedAppHostServerProjectto use them. - Adds
CSharpCliManagedAppHostModuleGenerator(module project generation),IntegrationClosureRestorer(build + closure materialization), and a newcs-managed-empty-apphostCLI template with feature-flag gating. - Wires CLI-managed AppHost support into
DotNetAppHostProject(run/publish/add-package),RestoreCommand,DotNetCliRunner(MSBuild property pass-through), and DI registration.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/Aspire.Cli/Projects/IntegrationClosureBuilder.cs |
New shared static helper owning MSBuild closure contract (file names, XML emission, closure reader, fingerprinting) |
src/Aspire.Cli/Projects/IntegrationClosureRestorer.cs |
New CLI-managed restore loop: generate module, build, read closure, emit probe manifest |
src/Aspire.Cli/Projects/CSharpCliManagedAppHostModuleGenerator.cs |
New module project + targets generator from aspire.config.json integrations |
src/Aspire.Cli/Projects/CSharpIntegrationProjectReferences.cs |
New shared helper resolving integration references as project or package references |
src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs |
Refactored to delegate closure logic to IntegrationClosureBuilder |
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs |
Refactored to use CSharpIntegrationProjectReferences.Resolve; internal visibility for helpers |
src/Aspire.Cli/Projects/DotNetAppHostProject.cs |
CLI-managed AppHost branching in run/publish/add-package/validate paths |
src/Aspire.Cli/DotNet/DotNetCliRunner.cs |
ProcessInvocationOptions.MSBuildProperties dictionary + pass-through in build/run/restore |
src/Aspire.Cli/Commands/RestoreCommand.cs |
CLI-managed restore path via IntegrationClosureRestorer |
src/Aspire.Cli/Templating/CliTemplateFactory.cs |
Register cs-managed-empty-apphost template behind feature flag |
src/Aspire.Cli/Templating/CliTemplateFactory.EmptyTemplate.cs |
Template apply + write methods for CLI-managed C# empty AppHost |
src/Aspire.Cli/Templating/KnownTemplateId.cs |
New CSharpCliManagedEmptyAppHost constant |
src/Aspire.Cli/Templating/Templates/cs-managed-empty-apphost/* |
Template files: apphost.cs, aspire.config.json, .vscode/extensions.json |
src/Aspire.Cli/KnownFeatures.cs |
New CSharpCliManagedAppHostEnabled feature flag |
src/Aspire.Cli/Program.cs |
DI registrations for module generator and closure restorer |
src/Aspire.Cli/Aspire.Cli.csproj |
Embedded resource config for new template |
playground/CliManagedCSharpAppHost/* |
Playground sample exercising end-to-end flow with Redis |
tests/Aspire.Cli.Tests/Projects/CSharpCliManagedAppHostModuleGeneratorTests.cs |
Unit tests for module generation (references, channels, feature gating) |
tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs |
Tests for CLI-managed run/publish/add-package/CanHandle paths |
tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs |
Updated to use IntegrationClosureBuilder constants |
tests/Aspire.Cli.Tests/Commands/RestoreCommandTests.cs |
Test for CLI-managed restore flow |
tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs |
Tests for template visibility and file-based AppHost creation |
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs |
DI registration updates for test service collection |
… probe manifest path - IntegrationClosureRestorer.RestoreAsync now honors PackageSourceOverride by routing through the rich TryGenerateAsync overload, so a subsequent regenerate doesn't drop RestoreAdditionalProjectSources/RestoreConfigFile. - CSharpIntegrationProjectReferences.Resolve takes privateProjectReferences (default true). CLI-managed module generator passes false so in-repo project refs flow into ReferenceCopyLocalPaths and the closure captures their outputs. Polyglot server keeps default behavior. - IntegrationClosureLayout.ProbeManifestPath is now nullable; TryLoad returns null when neither artifact is present and leaves ProbeManifestPath null when only the libs path exists. Consumer guards the env var on a non-empty path so we never wire a non-existent file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two issues found by end-to-end testing of the CLI-managed C# AppHost mode: 1. Freshly-created apphost.cs failed to compile with CS0103 on DistributedApplication because #:project ProjectReferences only contribute assembly references; they don't flow source-level global usings. Add 'using Aspire.Hosting;' to the cs-managed-empty-apphost template and the playground apphost so the synthesized file-based project compiles out of the box, matching the classic AppHost Program.cs convention. 2. 'aspire restore' with a missing or unresolvable integration package exited correctly but only printed 'See logs at ...' with no inline diagnostic. RestoreCommand now wires an OutputCollector through IntegrationClosureRestoreOptions.BuildInvocationOptions and calls DisplayLines on failure so users see the raw restore output (NU1101, NU1605, downgrade messages, source mappings, etc.) inline. The log file pointer is preserved for deeper investigation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Re-running the failed jobs in the CI workflow for this pull request because 3 jobs were identified as retry-safe transient failures in the CI run attempt.
|
Inline the generated CLI-managed module targets into Aspire.csproj instead of generating a separate Aspire.targets file, while deleting any stale targets file from older generations. Restore the existing AppHost server project reference behavior after the shared resolver refactor so TypeScript API compatibility can still scan Aspire.Hosting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| var hash = XxHash3.Hash(Encoding.UTF8.GetBytes(appHostFullPath)); | ||
| var hashFragment = Convert.ToHexString(hash)[..12].ToLowerInvariant(); |
There was a problem hiding this comment.
This does invalidate a previous restore (but not in a way that breaks restore, it's a one-time change on a subsequent run). Main thing is that SHA256 usage is discouraged for non-crypto uses like this so that we don't eventually get yelled at for using it like MD6, SHA1, etc. before it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| "path": "apphost.cs" | ||
| }, | ||
| "features": { | ||
| "csharpCliManagedAppHostEnabled": true |
There was a problem hiding this comment.
Any thoughts on this feature name?
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Generate AppHost-specific props and targets for CLI-managed C# AppHosts so CLI-owned builds get direct compile references and keep file AppHost build outputs under .aspire. Clear injected DirectoryBuild* properties from project references so referenced projects use their normal build conventions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
PR Testing ReportPR Information
CLI Version Verification
Changes AnalyzedFiles Changed
Change Categories
Test Scenarios ExecutedScenario 1: Install PR Dogfood CLIObjective: Install the PR CLI from the Dogfood comment and verify it matches the latest PR head. Evidence:
Scenario 2: Create CLI-managed C# AppHostObjective: Create a new Steps:
Evidence:
Scenario 3: Restore CLI-managed AppHost and generated moduleObjective: Verify Steps:
Evidence:
Scenario 4: Direct dotnet build guidanceObjective: Verify direct Expected Outcome: Non-zero exit code with guidance to use Observed: Evidence:
Scenario 5: Run empty CLI-managed AppHostObjective: Verify the generated empty AppHost builds and starts through the Aspire CLI. Steps:
Evidence:
Scenario 6: Add Redis integration and consume generated APIObjective: Verify Steps:
Evidence:
Summary
Overall Result✅ PR VERIFIED Notes
|
Canonicalize explicit AppHost paths to the filesystem path before invoking MSBuild or matching running AppHost sockets. This keeps macOS /var and /private/var path identities consistent across run, wait, describe, and stop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rely on the existing segment-walking path resolver for Unix filesystem path canonicalization instead of hardcoding macOS firmlink prefixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
❌ CLI E2E Tests failed — 114 passed, 1 failed, 2 unknown (commit ❌ Failed Tests
View all recordings
📹 Recordings uploaded automatically from CI run #27450820891 |
Description
Adds an experimental CLI-managed C# AppHost mode that mirrors the polyglot AppHost pattern: a thin single-file
apphost.csdeclares integration packages inaspire.config.json, and the Aspire CLI synthesizes/restores/builds a generatedAspire.csprojmodule under.aspire/modules/plus an integration closure cache under.aspire/integrations/apphosts/<hash>/. The single-file AppHost then probes integration assemblies from that cache at runtime via the sameASPIRE_INTEGRATION_LIBS_PATH/ASPIRE_INTEGRATION_PROBE_MANIFEST_PATHenv vars that the prebuilt polyglot AppHost server already uses.The goal is to move the C# AppHost toward the polyglot model (user code + CLI-orchestrated package restore) without a full server/RPC rework. Build and restore now work the same way for both modes; only the runtime hosting differs.
Approach
CSharpCliManagedAppHostModuleGeneratorwrites.aspire/modules/Aspire.csproj+Aspire.targetsfrom the integration list inaspire.config.json, with closure-emission MSBuild properties/targets injected so build produces the sameclosure-*.txtfiles the polyglot path consumes.IntegrationClosureRestorerowns the CLI-managed restore loop: generate module project, rundotnet restore/dotnet build, read closure files, materializeintegration-package-probe-manifest.jsonand the libs path next to the AppHost. Wired intoRestoreCommandand consumed byDotNetAppHostProjectto attach env vars to the launched AppHost.IntegrationClosureBuilderstatic helper owns the MSBuild closure contract (file-name constants,AddClosureProperties/AddClosureTargetsXML emission, closure file reader +project.assets.jsonfingerprinting). BothPrebuiltAppHostServer(polyglot) andIntegrationClosureRestorer(CLI-managed) delegate to it so the two consumers cannot drift on the contract. AClosureFileMissingBehaviorenum bridges divergent error semantics (polyglot throws, CLI-managed warns + returns null).cs-managed-empty-apphost(behind theCSharpCliManagedAppHostfeature flag inKnownFeatures) scaffolds anapphost.cs+aspire.config.jsonpair. The generated csproj is intentionally absent —aspire run/aspire restoreproduce the module project lazily.playground/CliManagedCSharpAppHostexercises the end-to-end flow with a Redis integration.Non-obvious bits worth a careful look
DotNetAppHostProjectbranches onisCliManagedSingleFileAppHostto take the new CLI-managed configuration path, which (unlike normal CliBundle AppHosts) always injects DCP/Dashboard env vars because the single-file AppHost doesn't ship per-RID NuGet metadata for them.Aspire.csprojshape the polyglot AppHost server emits, but it lives next to the AppHost (.aspire/modules/Aspire.csproj) so itsobj/project.assets.jsonis at.aspire/modules/obj/rather than under the closure restore directory. The shared closure reader takes the assets-file path as a parameter for exactly this reason.appsettings.jsoncontent is intentionally NOT shared between the two consumers — it's hashed into the closure manifest as a cache-invalidation signal, and the two paths emit different shapes (polyglot adds a"Logging"block + insertion-orderedAtsAssemblies; CLI-managed only emits sortedAtsAssemblies). The shared reader takes it as opaque input.CSharpCliManagedAppHostfeature flag and the playground sample uses it to opt in.Fixes # (issue)
Checklist
<remarks />and<code />elements on your triple slash comments?