From a67e74ba2dfa5660c4bd7197661cf5f54ea72f08 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 6 Nov 2026 16:08:19 -0800 Subject: [PATCH] Warn on unsupported AspireCliInvocationMode values (#21545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description `AspireCliInvocationMode` only recognized `Dnx` and `DnxPinned`; every other value (for example `DnxPinnedd` or `DefinitelyInvalid`) silently fell through to `Path` resolution, so typos never surfaced. **Root cause:** `_ResolveAspireCliInvocation` passed the raw value to `ResolveAspireCliInvocation`, which treats anything other than `Dnx`/`DnxPinned` as `Path`. The only validation target (`_ValidateAspireCliDnxInvocationMode`) is conditioned on a DNX mode, so it was skipped for unrecognized values. **Fix:** `_ResolveAspireCliInvocation` emits a new suppressible `ASPIRE014` warning when the value is not `Path`, `Dnx` or `DnxPinned` (case-insensitive), and resolution continues in `Path` mode as before. I chose a warning instead of an error so that existing builds keep working while the mistake becomes visible. `ASPIRE013` is skipped because open PRs #21288 and #19880 both propose it. `docs/list-of-diagnostics.md` documents `ASPIRE014`. Fixes #19714 ### Before (main) Playground `DotnetProject.AppHost` with `-p:AspireUseCliBundle=true -p:AspireCliInvocationMode=DefinitelyInvalid -t:_ResolveAspireCliInvocation -v:diag`: ```text Task Parameter:AspireCliInvocationMode=DefinitelyInvalid Aspire CLI invocation selection: Dnx (aspire: , dnx: /root/aspire/.dotnet/dnx) <- Path-mode fallback (no aspire on PATH) 0 Error(s) 0 Warning(s) ``` A full `dotnet build` of the AppHost with the same value also reported `0 Warning(s)`. ### After ```text Aspire.Hosting.AppHost.in.targets(221,5): warning ASPIRE014: DotnetProject.AppHost is configured with AspireCliInvocationMode=DefinitelyInvalid, which is not a supported value. Supported values are Path, Dnx, and DnxPinned. The default Path invocation mode will be used. Aspire CLI invocation selection: Dnx (aspire: , dnx: /root/aspire/.dotnet/dnx) 0 Error(s) 1 Warning(s) ``` Valid values (`Path`, `DnxPinned`) still resolve to `Aspire` and `Dnx` without a warning. ### End-to-end with Aspire I ran `dotnet run` on `playground/DotnetProject/DotnetProject.AppHost` with `AspireCliInvocationMode=DefinitelyInvalid` and the Aspire CLI built from this branch (`17.0.0-dev`) on `PATH`. The warning appears during the build, and the AppHost then launches through `aspire run` (Path mode): ```text Building... ...Aspire.Hosting.AppHost.in.targets(221,5): warning ASPIRE014: DotnetProject.AppHost is configured with AspireCliInvocationMode=DefinitelyInvalid, which is not a supported value. Supported values are Path, Dnx, and DnxPinned. The default Path invocation mode will be used. Connecting to AppHost... Starting dashboard... AppHost: DotnetProject.AppHost.csproj Press CTRL+C to stop the AppHost and exit. ``` (The dashboard was skipped locally with `SkipDashboardProjectReference=true`.) ### Tests New tests in `tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs`: - `ComputeRunArgumentsWarnsAndUsesPathModeWhenInvocationModeIsUnsupported` (`DefinitelyInvalid`, `DnxPinnedd`): asserts that `ASPIRE014` is emitted and that the fake `aspire` from `PATH` is selected. - `ComputeRunArgumentsDoesNotWarnWhenInvocationModeIsSupported` (unset, `Path`, `pAtH`, `Dnx`, `DnxPinned`). - `UnsupportedInvocationModeWarningCanBeSuppressed` (`NoWarn=ASPIRE014`). Without the fix, the two unsupported-value cases fail (`total: 8, failed: 2`). With the fix, the full `Aspire.Hosting.Sdk.Tests` project passes: 98 total, 94 passed, 0 failed, 4 skipped (Windows-only). ### Playground No playground file changed. In-repo playground AppHosts don't import `Aspire.Hosting.AppHost.props`, so `AspireUseCliBundle` isn't enabled by default and the packaged tasks assembly isn't present. Setting `AspireCliInvocationMode` in a playground project would therefore have no effect in a normal repo build. The playground proof above runs the existing `DotnetProject.AppHost` and passes those properties on the command line. ## Checklist - Is this feature complete? - [x] Yes. Ready to ship. - [ ] No. Follow-up changes expected. - Are you including unit tests for the changes and scenario tests if relevant? - [x] Yes - [ ] No - Did you add public API? - [ ] Yes - [x] No - Does the change make any security assumptions or guarantees? - [ ] Yes - [x] No **🤖 Posted by AspireAgent (AI Agent)** Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 7b48978d53dd834dd6a7ed6ee4d3097dae34f613) --- docs/list-of-diagnostics.md | 3 +- .../build/Aspire.Hosting.AppHost.in.targets | 4 + .../AppHostSdkTargetsTests.cs | 84 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 6546bdbf744..a493239de02 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -16,8 +16,9 @@ | `ASPIRE011` | Error | '\[ProjectName\]' is configured to invoke the Aspire CLI through DNX, but the dnx command could not be found on PATH. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) | | `ASPIRE012` | Warning | '\[ProjectName\]' uses the Aspire.AppHost.Sdk, which is obsolete but remains available for staged migration. Switch the project's Sdk attribute to Microsoft.NET.Sdk, add explicit PackageReference items for Aspire.Hosting.AppHost and Aspire.Hosting.Dotnet, and migrate C# resource ProjectReference items to AddDotnetProject calls. Set SuppressAspireAppHostSdkObsoleteWarning=true (or add ASPIRE012 to $(NoWarn)) to suppress temporarily during migration. See https://aka.ms/aspire/diagnostics/aspire012 for migration guidance. | [src/Aspire.AppHost.Sdk/SDK/Sdk.in.targets](../src/Aspire.AppHost.Sdk/SDK/Sdk.in.targets) | | `ASPIRE013` | Error | '\[ProjectName\]' requires the Aspire CLI bundle. Remove AspireUseCliBundle or set it to true, and install the matching Aspire CLI from https://get.aspire.dev. ASPIRE013 cannot be suppressed to disable the bundle. See https://aka.ms/aspire/diagnostics/aspire013 for more information. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) | +| `ASPIRE014` | Warning | '\[ProjectName\]' is configured with an unsupported AspireCliInvocationMode value. Supported values are Path, Dnx, and DnxPinned. The default Path invocation mode will be used. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) | -The CLI bundle is required by default for C# AppHosts and cannot be disabled (`ASPIRE013`). Non-C# AppHosts retain the suppressible `ASPIRE010` warning when the bundle is disabled. `AspireCliInvocationMode=Path` uses `aspire` from `PATH`, falling back to the `Aspire.Cli` version paired with the AppHost package through DNX when a compatible command is unavailable. `AspireCliInvocationMode=Dnx` invokes the unversioned `Aspire.Cli` package through DNX so an in-scope tool manifest is honored, or the latest package is used when no manifest applies. `AspireCliInvocationMode=DnxPinned` invokes the exact `Aspire.Cli` version paired with `Aspire.Hosting.AppHost`. +The CLI bundle is required by default for C# AppHosts and cannot be disabled (`ASPIRE013`). Non-C# AppHosts retain the suppressible `ASPIRE010` warning when the bundle is disabled. `AspireCliInvocationMode=Path` uses `aspire` from `PATH`, falling back to the `Aspire.Cli` version paired with the AppHost package through DNX when a compatible command is unavailable. `AspireCliInvocationMode=Dnx` invokes the unversioned `Aspire.Cli` package through DNX so an in-scope tool manifest is honored, or the latest package is used when no manifest applies. `AspireCliInvocationMode=DnxPinned` invokes the exact `Aspire.Cli` version paired with `Aspire.Hosting.AppHost`. Any other value produces the `ASPIRE014` warning and uses the default `Path` mode. When automatic CLI bundle setup fails, `ASPIRE009` includes the setup command's captured standard output and standard error, including DNX package acquisition errors. If a PATH CLI setup attempt falls back to DNX and neither produces a usable bundle, output from both attempts is included with the corresponding command. Successful or recovered setup attempts keep this output at diagnostic verbosity. diff --git a/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets b/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets index b8b23f2e441..81bad0a01be 100644 --- a/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets +++ b/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets @@ -226,6 +226,10 @@ namespace Projects%3B + + diff --git a/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs b/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs index fd977511171..7e6e7dd65a8 100644 --- a/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs +++ b/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs @@ -436,6 +436,84 @@ public async Task ComputeRunArgumentsFailsWhenDnxModeIsConfiguredAndDnxIsMissing Assert.Contains("Install or use the .NET SDK 10.0 or later", result.Output); } + [Theory] + [InlineData("DefinitelyInvalid")] + [InlineData("DnxPinnedd")] + public async Task ComputeRunArgumentsWarnsAndUsesPathModeWhenInvocationModeIsUnsupported(string invocationMode) + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli")); + var aspireCliPath = await CreateFakeAspireCliAsync(fakeCliDirectory.FullName); + await CreateFakeDnxAsync(fakeCliDirectory.FullName); + var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true, + $$""" + + {{invocationMode}} + + """); + + var result = await RunComputeRunArgumentsAsync(project, CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.True(result.ExitCode == 0, result.Output); + Assert.Contains("warning ASPIRE014", result.Output); + Assert.Contains($"AspireCliInvocationMode={invocationMode}, which is not a supported value", result.Output); + Assert.Contains("Supported values are Path, Dnx, and DnxPinned", result.Output); + + var properties = await GetComputeRunArgumentsPropertiesAsync( + project, + environment: CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.Equal("Aspire", properties["_AspireResolvedCliInvocationMode"]); + Assert.Equal(GetExpectedAspireRunCommand(aspireCliPath), properties["RunCommand"]); + } + + [Theory] + [InlineData(null)] + [InlineData("Path")] + [InlineData("pAtH")] + [InlineData("Dnx")] + [InlineData("DnxPinned")] + public async Task ComputeRunArgumentsDoesNotWarnWhenInvocationModeIsSupported(string? invocationMode) + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli")); + await CreateFakeAspireCliAsync(fakeCliDirectory.FullName); + await CreateFakeDnxAsync(fakeCliDirectory.FullName); + var extraProjectXml = invocationMode is null + ? null + : $$""" + + {{invocationMode}} + + """; + var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true, extraProjectXml); + + var result = await RunComputeRunArgumentsAsync(project, CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.True(result.ExitCode == 0, result.Output); + Assert.DoesNotContain("ASPIRE014", result.Output); + } + + [Fact] + public async Task UnsupportedInvocationModeWarningCanBeSuppressed() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli")); + await CreateFakeAspireCliAsync(fakeCliDirectory.FullName); + var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true, + """ + + DefinitelyInvalid + $(NoWarn);ASPIRE014 + + """); + + var result = await RunComputeRunArgumentsAsync(project, CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.True(result.ExitCode == 0, result.Output); + Assert.DoesNotContain("ASPIRE014", result.Output); + } + [Fact] public async Task ComputeRunArgumentsSkipsNonExecutableDnxOnUnix() { @@ -1640,6 +1718,12 @@ private static async Task> GetComputeRunArgumentsProp environment); } + private static Task RunComputeRunArgumentsAsync(RunHookProject project, IDictionary environment) + => RunDotNetWithArgumentsAsync( + project.ProjectDirectory, + ["msbuild", "-nologo", "-restore", "-t:ComputeRunArguments", project.ProjectFile], + environment); + private static async Task> GetResolveAspireCliBundlePathPropertiesAsync( RunHookProject project, IDictionary environment,