diff --git a/eng/Signing.props b/eng/Signing.props
index 1d50eda172e..236a68adcbf 100644
--- a/eng/Signing.props
+++ b/eng/Signing.props
@@ -26,6 +26,12 @@
+
+
+
+
+
+
diff --git a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs
index a61855bda5a..7558ea489b4 100644
--- a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs
+++ b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs
@@ -11,13 +11,13 @@ namespace Aspire.Cli.Backchannel;
internal sealed class AppHostBackchannel(ILogger logger, CliRpcTarget target)
{
- private readonly ActivitySource _activitySource = new(nameof(Aspire.Cli.Backchannel.AppHostBackchannel), "1.0.0");
+ private readonly ActivitySource _activitySource = new(nameof(AppHostBackchannel));
private readonly TaskCompletionSource _rpcTaskCompletionSource = new();
private Process? _process;
public async Task PingAsync(long timestamp, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(PingAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
var rpc = await _rpcTaskCompletionSource.Task;
@@ -37,7 +37,7 @@ public async Task RequestStopAsync(CancellationToken cancellationToken)
// of the AppHost process. The AppHost process will then trigger the shutdown
// which will allow the CLI to await the pending run.
- using var activity = _activitySource.StartActivity(nameof(RequestStopAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
var rpc = await _rpcTaskCompletionSource.Task;
@@ -51,7 +51,7 @@ await rpc.InvokeWithCancellationAsync(
public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(GetDashboardUrlsAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
var rpc = await _rpcTaskCompletionSource.Task;
@@ -67,7 +67,7 @@ await rpc.InvokeWithCancellationAsync(
public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(GetResourceStatesAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
var rpc = await _rpcTaskCompletionSource.Task;
@@ -88,7 +88,7 @@ await rpc.InvokeWithCancellationAsync(
public async Task ConnectAsync(Process process, string socketPath, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(ConnectAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
_process = process;
@@ -111,7 +111,7 @@ public async Task ConnectAsync(Process process, string socketPath, CancellationT
public async Task GetPublishersAsync(CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(GetPublishersAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false);
@@ -127,7 +127,7 @@ public async Task GetPublishersAsync(CancellationToken cancellationTok
public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(GetPublishingActivitiesAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
var rpc = await _rpcTaskCompletionSource.Task;
diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs
index 23c4380db1c..be68a9c02b6 100644
--- a/src/Aspire.Cli/Commands/AddCommand.cs
+++ b/src/Aspire.Cli/Commands/AddCommand.cs
@@ -11,42 +11,48 @@ namespace Aspire.Cli.Commands;
internal sealed class AddCommand : BaseCommand
{
- private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli");
+ private readonly ActivitySource _activitySource = new ActivitySource(nameof(AddCommand));
private readonly DotNetCliRunner _runner;
private readonly INuGetPackageCache _nuGetPackageCache;
- public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("add", "Add an integration or other resource to the Aspire project.")
+ public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
+ : base("add", "Add an integration to the Aspire project.")
{
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache));
_runner = runner;
_nuGetPackageCache = nuGetPackageCache;
- var resourceArgument = new Argument("resource");
- resourceArgument.Arity = ArgumentArity.ZeroOrOne;
- Arguments.Add(resourceArgument);
+ var integrationArgument = new Argument("integration");
+ integrationArgument.Description = "The name of the integration to add (e.g. redis, postgres).";
+ integrationArgument.Arity = ArgumentArity.ZeroOrOne;
+ Arguments.Add(integrationArgument);
var projectOption = new Option("--project");
+ projectOption.Description = "The path to the project file to add the integration to.";
projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption);
Options.Add(projectOption);
var versionOption = new Option("--version", "-v");
+ versionOption.Description = "The version of the integration to add.";
Options.Add(versionOption);
var prereleaseOption = new Option("--prerelease");
+ prereleaseOption.Description = "Include pre-release versions of the integration when searching.";
Options.Add(prereleaseOption);
var sourceOption = new Option("--source", "-s");
+ sourceOption.Description = "The NuGet source to use for the integration.";
Options.Add(sourceOption);
}
protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal);
+ using var activity = _activitySource.StartActivity();
try
{
- var integrationName = parseResult.GetValue("resource");
+ var integrationName = parseResult.GetValue("integration");
var passedAppHostProjectFile = parseResult.GetValue("--project");
var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile);
@@ -62,7 +68,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
var packages = await AnsiConsole.Status().StartAsync(
"Searching for Aspire packages...",
- context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile, prerelease, source, cancellationToken)
+ context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile.Directory!, prerelease, source, cancellationToken)
);
var version = parseResult.GetValue("--version");
diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs
index 7cbc4f104e1..b8f174f2b23 100644
--- a/src/Aspire.Cli/Commands/NewCommand.cs
+++ b/src/Aspire.Cli/Commands/NewCommand.cs
@@ -2,45 +2,50 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.CommandLine;
-using System.CommandLine.Parsing;
using System.Diagnostics;
using Aspire.Cli.Utils;
+using Semver;
using Spectre.Console;
namespace Aspire.Cli.Commands;
internal sealed class NewCommand : BaseCommand
{
- private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli");
+ private readonly ActivitySource _activitySource = new ActivitySource(nameof(NewCommand));
private readonly DotNetCliRunner _runner;
+ private readonly INuGetPackageCache _nuGetPackageCache;
- public NewCommand(DotNetCliRunner runner) : base("new", "Create a new Aspire sample project.")
+ public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
+ : base("new", "Create a new Aspire sample project.")
{
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
+ ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache));
_runner = runner;
+ _nuGetPackageCache = nuGetPackageCache;
var templateArgument = new Argument("template");
- templateArgument.Validators.Add(ValidateProjectTemplate);
+ templateArgument.Description = "The name of the project template to use (e.g. aspire-starter, aspire).";
templateArgument.Arity = ArgumentArity.ZeroOrOne;
Arguments.Add(templateArgument);
var nameOption = new Option("--name", "-n");
+ nameOption.Description = "The name of the project to create.";
Options.Add(nameOption);
var outputOption = new Option("--output", "-o");
+ outputOption.Description = "The output path for the project.";
Options.Add(outputOption);
-
- var prereleaseOption = new Option("--prerelease");
- Options.Add(prereleaseOption);
var sourceOption = new Option("--source", "-s");
+ sourceOption.Description = "The NuGet source to use for the project templates.";
Options.Add(sourceOption);
var templateVersionOption = new Option("--version", "-v");
+ templateVersionOption.Description = "The version of the project templates to use.";
Options.Add(templateVersionOption);
}
- private static void ValidateProjectTemplate(ArgumentResult result)
+ private static async Task<(string TemplateName, string TemplateDescription, string? PathAppendage)> GetProjectTemplateAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
// TODO: We need to integrate with the template engine to interrogate
// the list of available templates. For now we will just hard-code
@@ -49,55 +54,91 @@ private static void ValidateProjectTemplate(ArgumentResult result)
// Once we integrate with template engine we will also be able to
// interrogate the various options and add them. For now we will
// keep it simple.
- string[] validTemplates = [
- "aspire-starter",
- "aspire",
- "aspire-apphost",
- "aspire-servicedefaults",
- "aspire-mstest",
- "aspire-nunit",
- "aspire-xunit"
+ (string TemplateName, string TemplateDescription, string? PathAppendage)[] validTemplates = [
+ ("aspire-starter", "Aspire Starter App", "src") ,
+ ("aspire", "Aspire Empty App", "src"),
+ ("aspire-apphost", "Aspire App Host", null),
+ ("aspire-servicedefaults", "Aspire Service Defaults", null),
+ ("aspire-mstest", "Aspire Test Project (MSTest)", null),
+ ("aspire-nunit", "Aspire Test Project (NUnit)", null),
+ ("aspire-xunit", "Aspire Test Project (xUnit)", null)
];
- var value = result.GetValueOrDefault();
-
- if (value is null)
+ if (parseResult.GetValue("template") is { } templateName && validTemplates.SingleOrDefault(t => t.TemplateName == templateName) is { } template)
{
- // This is OK, for now we will use the default
- // template of aspire-starter, but we might
- // be able to do more intelligent selection in the
- // future based on what is already in the working directory.
- return;
+ return template;
}
-
- if (value is { } templateName && !validTemplates.Contains(templateName))
+ else
{
- result.AddError($"The specified template '{templateName}' is not valid. Valid templates are [{string.Join(", ", validTemplates)}].");
- return;
+ return await PromptUtils.PromptForSelectionAsync(
+ "Select a project template:",
+ validTemplates,
+ t => $"{t.TemplateName} ({t.TemplateDescription})",
+ cancellationToken
+ );
}
}
- protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
+ private static async Task GetProjectNameAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal);
+ if (parseResult.GetValue("--name") is not { } name)
+ {
+ var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name;
+ name = await PromptUtils.PromptForStringAsync("Enter the project name:",
+ defaultValue: defaultName,
+ cancellationToken: cancellationToken);
+ }
- var templateVersion = parseResult.GetValue("--version");
- var prerelease = parseResult.GetValue("--prerelease");
+ return name;
+ }
- if (templateVersion is not null && prerelease)
+ private static async Task GetOutputPathAsync(ParseResult parseResult, string? pathAppendage, CancellationToken cancellationToken)
+ {
+ if (parseResult.GetValue("--output") is not { } outputPath)
{
- AnsiConsole.MarkupLine("[red bold]:thumbs_down: The --version and --prerelease options are mutually exclusive.[/]");
- return ExitCodeConstants.FailedToCreateNewProject;
+ outputPath = await PromptUtils.PromptForStringAsync(
+ "Enter the output path:",
+ defaultValue: Path.Combine(Environment.CurrentDirectory, pathAppendage ?? string.Empty),
+ cancellationToken: cancellationToken
+ );
}
- else if (prerelease)
+
+ return Path.GetFullPath(outputPath);
+ }
+
+ private static async Task GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ if (parseResult.GetValue("--version") is { } version)
{
- templateVersion = "*-*";
+ return version;
}
- else if (templateVersion is null)
+ else
{
- templateVersion = VersionHelper.GetDefaultTemplateVersion();
+ version = await PromptUtils.PromptForStringAsync(
+ "Project templates version:",
+ defaultValue: VersionHelper.GetDefaultTemplateVersion(),
+ validator: (string value) => {
+ if (SemVersion.TryParse(value, out var parsedVersion))
+ {
+ return ValidationResult.Success();
+ }
+
+ return ValidationResult.Error("Invalid version format. Please enter a valid version.");
+ },
+ cancellationToken);
+
+ return version;
}
+ }
+ protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ using var activity = _activitySource.StartActivity();
+
+ var template = await GetProjectTemplateAsync(parseResult, cancellationToken);
+ var name = await GetProjectNameAsync(parseResult, cancellationToken);
+ var outputPath = await GetOutputPathAsync(parseResult, template.PathAppendage, cancellationToken);
+ var version = await GetProjectTemplatesVersionAsync(parseResult, cancellationToken);
var source = parseResult.GetValue("--source");
var templateInstallResult = await AnsiConsole.Status()
@@ -106,7 +147,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
.StartAsync(
":ice: Getting latest templates...",
async context => {
- return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", templateVersion!, source, true, cancellationToken);
+ return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken);
});
if (templateInstallResult.ExitCode != 0)
@@ -117,23 +158,6 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
AnsiConsole.MarkupLine($":package: Using project templates version: {templateInstallResult.TemplateVersion}");
- var templateName = parseResult.GetValue("template") ?? "aspire-starter";
-
- if (parseResult.GetValue("--output") is not { } outputPath)
- {
- outputPath = Environment.CurrentDirectory;
- }
- else
- {
- outputPath = Path.GetFullPath(outputPath);
- }
-
- if (parseResult.GetValue("--name") is not { } name)
- {
- var outputPathDirectoryInfo = new DirectoryInfo(outputPath);
- name = outputPathDirectoryInfo.Name;
- }
-
int newProjectExitCode = await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots3)
.SpinnerStyle(Style.Parse("purple"))
@@ -141,11 +165,11 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
":rocket: Creating new Aspire project...",
async context => {
return await _runner.NewProjectAsync(
- templateName,
- name,
- outputPath,
- cancellationToken);
- });
+ template.TemplateName,
+ name,
+ outputPath,
+ cancellationToken);
+ });
if (newProjectExitCode != 0)
{
diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs
index 496a282bf2f..df9c19d8d97 100644
--- a/src/Aspire.Cli/Commands/PublishCommand.cs
+++ b/src/Aspire.Cli/Commands/PublishCommand.cs
@@ -12,29 +12,33 @@ namespace Aspire.Cli.Commands;
internal sealed class PublishCommand : BaseCommand
{
- private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli");
+ private readonly ActivitySource _activitySource = new ActivitySource(nameof(PublishCommand));
private readonly DotNetCliRunner _runner;
- public PublishCommand(DotNetCliRunner runner) : base("publish", "Generates deployment artifacts for an Aspire app host project.")
+ public PublishCommand(DotNetCliRunner runner)
+ : base("publish", "Generates deployment artifacts for an Aspire app host project.")
{
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
_runner = runner;
var projectOption = new Option("--project");
+ projectOption.Description = "The path to the Aspire app host project file.";
projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption);
Options.Add(projectOption);
var publisherOption = new Option("--publisher", "-p");
+ publisherOption.Description = "The name of the publisher to use.";
Options.Add(publisherOption);
var outputPath = new Option("--output-path", "-o");
+ outputPath.Description = "The output path for the generated artifacts.";
outputPath.DefaultValueFactory = (result) => Path.Combine(Environment.CurrentDirectory);
Options.Add(outputPath);
}
protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal);
+ using var activity = _activitySource.StartActivity();
var passedAppHostProjectFile = parseResult.GetValue("--project");
var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile);
diff --git a/src/Aspire.Cli/Commands/RootCommand.cs b/src/Aspire.Cli/Commands/RootCommand.cs
index a2f7cd1db05..22d4ad6bbdf 100644
--- a/src/Aspire.Cli/Commands/RootCommand.cs
+++ b/src/Aspire.Cli/Commands/RootCommand.cs
@@ -14,13 +14,16 @@ namespace Aspire.Cli.Commands;
internal sealed class RootCommand : BaseRootCommand
{
- public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addCommand, PublishCommand publishCommand) : base("Aspire CLI")
+ public RootCommand(NewCommand newCommand, RunCommand runCommand, AddCommand addCommand, PublishCommand publishCommand)
+ : base("The Aspire CLI can be used to create, run, and publish Aspire-based applications.")
{
var debugOption = new Option("--debug", "-d");
+ debugOption.Description = "Enable debug logging to the console.";
debugOption.Recursive = true;
Options.Add(debugOption);
var waitForDebuggerOption = new Option("--wait-for-debugger", "-w");
+ waitForDebuggerOption.Description = "Wait for a debugger to attach before executing the command.";
waitForDebuggerOption.Recursive = true;
waitForDebuggerOption.DefaultValueFactory = (result) => false;
diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs
index 566667fd8d1..bb992a8fee7 100644
--- a/src/Aspire.Cli/Commands/RunCommand.cs
+++ b/src/Aspire.Cli/Commands/RunCommand.cs
@@ -14,26 +14,29 @@ namespace Aspire.Cli.Commands;
internal sealed class RunCommand : BaseCommand
{
- private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli");
+ private readonly ActivitySource _activitySource = new ActivitySource(nameof(RunCommand));
private readonly DotNetCliRunner _runner;
- public RunCommand(DotNetCliRunner runner) : base("run", "Run an Aspire app host in development mode.")
+ public RunCommand(DotNetCliRunner runner)
+ : base("run", "Run an Aspire app host in development mode.")
{
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
_runner = runner;
var projectOption = new Option("--project");
+ projectOption.Description = "The path to the Aspire app host project file.";
projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption);
Options.Add(projectOption);
var watchOption = new Option("--watch", "-w");
+ watchOption.Description = "Start project resources in watch mode.";
Options.Add(watchOption);
}
protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal);
+ using var activity = _activitySource.StartActivity();
var passedAppHostProjectFile = parseResult.GetValue("--project");
var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile);
diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs
index ae8188cd700..40eafffa202 100644
--- a/src/Aspire.Cli/DotNetCliRunner.cs
+++ b/src/Aspire.Cli/DotNetCliRunner.cs
@@ -15,13 +15,13 @@ namespace Aspire.Cli;
internal sealed class DotNetCliRunner(ILogger logger, IServiceProvider serviceProvider)
{
- private readonly ActivitySource _activitySource = new ActivitySource(nameof(Aspire.Cli.DotNetCliRunner));
+ private readonly ActivitySource _activitySource = new ActivitySource(nameof(DotNetCliRunner));
internal Func GetCurrentProcessId { get; set; } = () => Environment.ProcessId;
public async Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(FileInfo projectFile, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(GetAppHostInformationAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
string[] cliArgs = ["msbuild", "-getproperty:IsAspireHost,AspireHostingSDKVersion"];
@@ -79,7 +79,7 @@ internal sealed class DotNetCliRunner(ILogger logger, IServiceP
public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(RunAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
if (watch && noBuild)
{
@@ -100,7 +100,7 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild,
public async Task CheckHttpCertificateAsync(CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(CheckHttpCertificateAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
string[] cliArgs = ["dev-certs", "https", "--check", "--trust"];
return await ExecuteAsync(
@@ -114,7 +114,7 @@ public async Task CheckHttpCertificateAsync(CancellationToken cancellationT
public async Task TrustHttpCertificateAsync(CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(TrustHttpCertificateAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
string[] cliArgs = ["dev-certs", "https", "--trust"];
return await ExecuteAsync(
@@ -230,7 +230,7 @@ private static bool TryParsePackageVersionFromStdout(string stdout, [NotNullWhen
public async Task NewProjectAsync(string templateName, string name, string outputPath, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(NewProjectAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
string[] cliArgs = ["new", templateName, "--name", name, "--output", outputPath];
return await ExecuteAsync(
@@ -259,7 +259,7 @@ internal static string GetBackchannelSocketPath()
public async Task ExecuteAsync(string[] args, IDictionary? env, DirectoryInfo workingDirectory, TaskCompletionSource? backchannelCompletionSource, Action? streamsCallback, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(ExecuteAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
var startInfo = new ProcessStartInfo("dotnet")
{
@@ -379,7 +379,7 @@ async Task ForwardStreamToLoggerAsync(StreamReader reader, string identifier, Pr
private async Task StartBackchannelAsync(Process process, string socketPath, TaskCompletionSource backchannelCompletionSource, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(StartBackchannelAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50));
@@ -431,7 +431,7 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas
public async Task BuildAsync(FileInfo projectFilePath, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(BuildAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
string[] cliArgs = ["build", projectFilePath.FullName];
return await ExecuteAsync(
@@ -444,7 +444,7 @@ public async Task BuildAsync(FileInfo projectFilePath, CancellationToken ca
}
public async Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(AddPackageAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
string[] cliArgs = [
"add",
@@ -477,9 +477,9 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN
return result;
}
- public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(FileInfo projectFilePath, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken)
+ public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(SearchPackagesAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
List cliArgs = [
"package",
@@ -510,7 +510,7 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN
var result = await ExecuteAsync(
args: cliArgs.ToArray(),
env: null,
- workingDirectory: projectFilePath.Directory!,
+ workingDirectory: workingDirectory!,
backchannelCompletionSource: null,
streamsCallback: (_, output, _) => {
// We need to read the output of the streams
@@ -550,6 +550,12 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN
foreach (var packageResult in sourcePackagesArray.EnumerateArray())
{
var id = packageResult.GetProperty("id").GetString();
+
+ // var version = prerelease switch {
+ // true => packageResult.GetProperty("version").GetString(),
+ // false => packageResult.GetProperty("latestVersion").GetString()
+ // };
+
var version = packageResult.GetProperty("latestVersion").GetString();
foundPackages.Add(new NuGetPackage
diff --git a/src/Aspire.Cli/NuGetPackageCache.cs b/src/Aspire.Cli/NuGetPackageCache.cs
index ef90821c873..402c99a9f92 100644
--- a/src/Aspire.Cli/NuGetPackageCache.cs
+++ b/src/Aspire.Cli/NuGetPackageCache.cs
@@ -8,18 +8,18 @@ namespace Aspire.Cli;
internal interface INuGetPackageCache
{
- Task> GetPackagesAsync(FileInfo projectFile, bool prerelease, string? source, CancellationToken cancellationToken);
+ Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken);
}
internal sealed class NuGetPackageCache(ILogger logger, DotNetCliRunner cliRunner) : INuGetPackageCache
{
- private readonly ActivitySource _activitySource = new(nameof(Aspire.Cli.NuGetPackageCache), "1.0.0");
+ private readonly ActivitySource _activitySource = new(nameof(NuGetPackageCache));
private const int SearchPageSize = 100;
- public async Task> GetPackagesAsync(FileInfo projectFile, bool prerelease, string? source, CancellationToken cancellationToken)
+ public async Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken)
{
- using var activity = _activitySource.StartActivity(nameof(GetPackagesAsync), ActivityKind.Client);
+ using var activity = _activitySource.StartActivity();
logger.LogDebug("Getting integrations from NuGet");
@@ -31,7 +31,7 @@ public async Task> GetPackagesAsync(FileInfo projectFi
{
// This search should pick up Aspire.Hosting.* and CommunityToolkit.Aspire.Hosting.*
var result = await cliRunner.SearchPackagesAsync(
- projectFile,
+ workingDirectory,
"Aspire.Hosting",
prerelease,
SearchPageSize,
diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs
index 37ec3f33636..9d86c0ee70a 100644
--- a/src/Aspire.Cli/Program.cs
+++ b/src/Aspire.Cli/Program.cs
@@ -10,13 +10,15 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
using RootCommand = Aspire.Cli.Commands.RootCommand;
namespace Aspire.Cli;
public class Program
{
- private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(Aspire.Cli.Program));
+ private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(Program));
private static IHost BuildApplication(string[] args)
{
@@ -30,14 +32,22 @@ private static IHost BuildApplication(string[] args)
logging.IncludeScopes = true;
});
- var otelBuilder = builder.Services.AddOpenTelemetry()
- .WithTracing(tracing => {
- tracing.AddSource(
- nameof(Aspire.Cli.NuGetPackageCache),
- nameof(Aspire.Cli.Backchannel.AppHostBackchannel),
- nameof(Aspire.Cli.DotNetCliRunner),
- nameof(Aspire.Cli.Program));
- });
+ var otelBuilder = builder.Services
+ .AddOpenTelemetry()
+ .WithTracing(tracing => {
+ tracing.AddSource(
+ nameof(NuGetPackageCache),
+ nameof(AppHostBackchannel),
+ nameof(DotNetCliRunner),
+ nameof(Program),
+ nameof(NewCommand),
+ nameof(RunCommand),
+ nameof(AddCommand),
+ nameof(PublishCommand)
+ );
+
+ tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("aspire-cli"));
+ });
if (builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] is {})
{
@@ -84,7 +94,7 @@ public static async Task Main(string[] args)
var config = new CommandLineConfiguration(rootCommand);
config.EnableDefaultExceptionHandler = true;
- using var activity = s_activitySource.StartActivity(nameof(Main), ActivityKind.Internal);
+ using var activity = s_activitySource.StartActivity();
var exitCode = await config.InvokeAsync(args);
await app.StopAsync().ConfigureAwait(false);
diff --git a/src/Aspire.Cli/Utils/PromptUtils.cs b/src/Aspire.Cli/Utils/PromptUtils.cs
new file mode 100644
index 00000000000..4b00b885e1d
--- /dev/null
+++ b/src/Aspire.Cli/Utils/PromptUtils.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Spectre.Console;
+
+namespace Aspire.Cli.Utils;
+
+internal static class PromptUtils
+{
+ public static async Task PromptForStringAsync(string promptText, string? defaultValue = null, Func? validator = null, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(promptText, nameof(promptText));
+ var prompt = new TextPrompt(promptText);
+
+ if (defaultValue is not null)
+ {
+ prompt.DefaultValue(defaultValue);
+ prompt.ShowDefaultValue();
+ }
+
+ if (validator is not null)
+ {
+ prompt.Validate(validator);
+ }
+
+ return await AnsiConsole.PromptAsync(prompt, cancellationToken);
+ }
+
+ public static async Task PromptForSelectionAsync(string promptText, IEnumerable choices, Func choiceFormatter, CancellationToken cancellationToken = default) where T: notnull
+ {
+ ArgumentNullException.ThrowIfNull(promptText, nameof(promptText));
+ ArgumentNullException.ThrowIfNull(choices, nameof(choices));
+ ArgumentNullException.ThrowIfNull(choiceFormatter, nameof(choiceFormatter));
+
+ var prompt = new SelectionPrompt()
+ .Title(promptText)
+ .UseConverter(choiceFormatter)
+ .AddChoices(choices)
+ .PageSize(10)
+ .EnableSearch()
+ .HighlightStyle(Style.Parse("darkmagenta"));
+
+ return await AnsiConsole.PromptAsync(prompt, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs
index ce17e9813b3..276cce3e9f6 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs
+++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs
@@ -16,42 +16,42 @@ public class AzureContainerAppEnvironmentResource(string name, Action
/// Gets the unique identifier of the Container App Environment.
///
- public BicepOutputReference ContainerAppEnvironmentId => new("AZURE_CONTAINER_APPS_ENVIRONMENT_ID", this);
+ private BicepOutputReference ContainerAppEnvironmentId => new("AZURE_CONTAINER_APPS_ENVIRONMENT_ID", this);
///
/// Gets the default domain associated with the Container App Environment.
///
- public BicepOutputReference ContainerAppDomain => new("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", this);
+ private BicepOutputReference ContainerAppDomain => new("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", this);
///
/// Gets the URL endpoint of the associated Azure Container Registry.
///
- public BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this);
+ private BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this);
///
/// Gets the managed identity ID associated with the Azure Container Registry.
///
- public BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this);
+ private BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this);
///
/// Gets the unique identifier of the Log Analytics workspace.
///
- public BicepOutputReference LogAnalyticsWorkspaceId => new("AZURE_LOG_ANALYTICS_WORKSPACE_ID", this);
+ private BicepOutputReference LogAnalyticsWorkspaceId => new("AZURE_LOG_ANALYTICS_WORKSPACE_ID", this);
///
/// Gets the principal name of the managed identity.
///
- public BicepOutputReference PrincipalName => new("MANAGED_IDENTITY_NAME", this);
+ private BicepOutputReference PrincipalName => new("MANAGED_IDENTITY_NAME", this);
///
/// Gets the principal ID of the managed identity.
///
- public BicepOutputReference PrincipalId => new("MANAGED_IDENTITY_PRINCIPAL_ID", this);
+ private BicepOutputReference PrincipalId => new("MANAGED_IDENTITY_PRINCIPAL_ID", this);
///
/// Gets the name of the Container App Environment.
///
- public BicepOutputReference ContainerAppEnvironmentName => new("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", this);
+ private BicepOutputReference ContainerAppEnvironmentName => new("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", this);
internal Dictionary VolumeNames { get; } = [];
diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs
index 2f0a1a3ccbe..cfce5b42e64 100644
--- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs
+++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs
@@ -714,7 +714,7 @@ BicepValue GetHostValue(string? prefix = null, string? suffix = null)
return (AllocateParameter(secretOutputReference, secretType: SecretType.KeyVault), SecretType.KeyVault);
}
- if (value is IKeyVaultSecretReference vaultSecretReference)
+ if (value is IAzureKeyVaultSecretReference vaultSecretReference)
{
if (parent is null)
{
@@ -797,7 +797,7 @@ private BicepValue AllocateKeyVaultSecretUriReference(BicepSecretOutputR
return secret.Properties.SecretUri;
}
- private BicepValue AllocateKeyVaultSecretUriReference(IKeyVaultSecretReference secretOutputReference)
+ private BicepValue AllocateKeyVaultSecretUriReference(IAzureKeyVaultSecretReference secretOutputReference)
{
if (!KeyVaultRefs.TryGetValue(secretOutputReference.Resource.Name, out var kv))
{
diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
index d0e027c8277..8bdeb655259 100644
--- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
+++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
@@ -342,6 +342,20 @@ public static IResourceBuilder WithAccessKeyAuthenticatio
var kv = builder.ApplicationBuilder.AddAzureKeyVault($"{builder.Resource.Name}-kv")
.WithParentRelationship(builder.Resource);
+ // remove the KeyVault from the model if the emulator is used during run mode.
+ // need to do this later in case builder becomes an emulator after this method is called.
+ if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
+ {
+ builder.ApplicationBuilder.Eventing.Subscribe((data, _) =>
+ {
+ if (builder.Resource.IsEmulator)
+ {
+ data.Model.Resources.Remove(kv.Resource);
+ }
+ return Task.CompletedTask;
+ });
+ }
+
return builder.WithAccessKeyAuthentication(kv);
}
@@ -351,7 +365,7 @@ public static IResourceBuilder WithAccessKeyAuthenticatio
/// The Azure Cosmos DB resource builder.
/// The Azure Key Vault resource builder where the connection string used to connect to this AzureCosmosDBResource will be stored.
/// A reference to the builder.
- public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder)
+ public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder)
{
ArgumentNullException.ThrowIfNull(builder);
diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs
index eb9cb02fc62..f21cccc6839 100644
--- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs
+++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs
@@ -41,7 +41,7 @@ public class AzureCosmosDBResource(string name, Action
- internal IKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; }
+ internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; }
private BicepOutputReference NameOutputReference => new("name", this);
@@ -134,7 +134,7 @@ internal ReferenceExpression GetChildConnectionString(string childResourceName,
var builder = new ReferenceExpressionBuilder();
- if (UseAccessKeyAuthentication)
+ if (UseAccessKeyAuthentication && !IsEmulator)
{
builder.AppendFormatted(ConnectionStringSecretOutput.Resource.GetSecretReference(GetKeyValueSecretName(childResourceName)));
}
diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs
index bfae31c431e..bd6a3580cb8 100644
--- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs
+++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs
@@ -13,7 +13,7 @@ namespace Aspire.Hosting.Azure;
/// The name of the resource.
/// Callback to configure the Azure resources.
public class AzureKeyVaultResource(string name, Action configureInfrastructure)
- : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IKeyVaultResource
+ : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IAzureKeyVaultResource
{
///
/// Gets the "vaultUri" output reference for the Azure Key Vault resource.
@@ -31,12 +31,12 @@ public class AzureKeyVaultResource(string name, Action
ReferenceExpression.Create($"{VaultUri}");
- BicepOutputReference IKeyVaultResource.VaultUriOutputReference => VaultUri;
+ BicepOutputReference IAzureKeyVaultResource.VaultUriOutputReference => VaultUri;
// In run mode, this is set to the secret client used to access the Azure Key Vault.
- internal Func>? SecretResolver { get; set; }
+ internal Func>? SecretResolver { get; set; }
- Func>? IKeyVaultResource.SecretResolver
+ Func>? IAzureKeyVaultResource.SecretResolver
{
get => SecretResolver;
set => SecretResolver = value;
@@ -48,7 +48,7 @@ public class AzureKeyVaultResource(string name, Action
///
///
- public IKeyVaultSecretReference GetSecretReference(string secretName)
+ public IAzureKeyVaultSecretReference GetSecretReference(string secretName)
{
ArgumentException.ThrowIfNullOrEmpty(secretName, nameof(secretName));
diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs
index 3f078529b32..b4cfb718996 100644
--- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs
+++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultSecretReference.cs
@@ -10,7 +10,7 @@ namespace Aspire.Hosting.Azure;
///
/// The name of the secret.
/// The Azure Key Vault resource.
-internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVaultResource azureKeyVaultResource) : IKeyVaultSecretReference, IValueProvider, IManifestExpressionProvider
+internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVaultResource azureKeyVaultResource) : IAzureKeyVaultSecretReference, IValueProvider, IManifestExpressionProvider
{
///
/// Gets the name of the secret.
@@ -20,7 +20,7 @@ internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVa
///
/// Gets the Azure Key Vault resource.
///
- public IKeyVaultResource Resource => azureKeyVaultResource;
+ public IAzureKeyVaultResource Resource => azureKeyVaultResource;
string IManifestExpressionProvider.ValueExpression => $"{{{azureKeyVaultResource.Name}.secrets.{SecretName}}}";
@@ -28,7 +28,7 @@ internal sealed class AzureKeyVaultSecretReference(string secretName, AzureKeyVa
{
if (azureKeyVaultResource.SecretResolver is { } secretResolver)
{
- return await secretResolver(secretName, cancellationToken).ConfigureAwait(false);
+ return await secretResolver(this, cancellationToken).ConfigureAwait(false);
}
throw new InvalidOperationException($"Secret '{secretName}' not found in Key Vault '{azureKeyVaultResource.Name}'.");
diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs
index 59902d48d8f..60a279045d7 100644
--- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs
+++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs
@@ -118,7 +118,7 @@ public static IResourceBuilder AsAzurePostgresFlexibleSe
/// This requires changes to the application code to use an azure credential to authenticate with the resource. See
/// https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-connect-with-managed-identity#connect-using-managed-identity-in-c for more information.
///
- /// You can use the method to configure the resource to use password authentication.
+ /// You can use the method to configure the resource to use password authentication.
///
///
/// The following example creates an Azure PostgreSQL Flexible Server resource and referencing that resource in a .NET project.
@@ -289,6 +289,20 @@ public static IResourceBuilder WithPassword
var kv = builder.ApplicationBuilder.AddAzureKeyVault($"{builder.Resource.Name}-kv")
.WithParentRelationship(builder.Resource);
+ // remove the KeyVault from the model if the emulator is used during run mode.
+ // need to do this later in case builder becomes an emulator after this method is called.
+ if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
+ {
+ builder.ApplicationBuilder.Eventing.Subscribe((data, token) =>
+ {
+ if (builder.Resource.IsContainer())
+ {
+ data.Model.Resources.Remove(kv.Resource);
+ }
+ return Task.CompletedTask;
+ });
+ }
+
return builder.WithPasswordAuthentication(kv, userName, password);
}
@@ -303,7 +317,7 @@ public static IResourceBuilder WithPassword
/// A reference to the builder.
public static IResourceBuilder WithPasswordAuthentication(
this IResourceBuilder builder,
- IResourceBuilder keyVaultBuilder,
+ IResourceBuilder keyVaultBuilder,
IResourceBuilder? userName = null,
IResourceBuilder? password = null)
{
diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs
index 34ddbab1d72..0946756806c 100644
--- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs
+++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs
@@ -33,7 +33,7 @@ public class AzurePostgresFlexibleServerResource(string name, Action
- internal IKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; }
+ internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; }
private BicepOutputReference NameOutputReference => new("name", this);
diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs
index 7c7b844c4e3..ec1cdfee8a9 100644
--- a/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs
+++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisCacheResource.cs
@@ -29,7 +29,7 @@ public class AzureRedisCacheResource(string name, Action
- internal IKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; }
+ internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; }
private BicepOutputReference NameOutputReference => new("name", this);
diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs
index 0795cebc879..a0171036463 100644
--- a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs
+++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs
@@ -96,7 +96,7 @@ public static IResourceBuilder AsAzureRedis(this IResourceBuilder
/// This requires changes to the application code to use an azure credential to authenticate with the resource. See
/// https://github.com/Azure/Microsoft.Azure.StackExchangeRedis for more information.
///
- /// You can use the method to configure the resource to use access key authentication.
+ /// You can use the method to configure the resource to use access key authentication.
///
///
/// The following example creates an Azure Cache for Redis resource and referencing that resource in a .NET project.
@@ -195,6 +195,20 @@ public static IResourceBuilder WithAccessKeyAuthenticat
var kv = builder.ApplicationBuilder.AddAzureKeyVault($"{builder.Resource.Name}-kv")
.WithParentRelationship(builder.Resource);
+ // remove the KeyVault from the model if the emulator is used during run mode.
+ // need to do this later in case builder becomes an emulator after this method is called.
+ if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
+ {
+ builder.ApplicationBuilder.Eventing.Subscribe((data, token) =>
+ {
+ if (builder.Resource.IsContainer())
+ {
+ data.Model.Resources.Remove(kv.Resource);
+ }
+ return Task.CompletedTask;
+ });
+ }
+
return builder.WithAccessKeyAuthentication(kv);
}
@@ -204,7 +218,7 @@ public static IResourceBuilder WithAccessKeyAuthenticat
/// The Azure Cache for Redis resource builder.
/// The Azure Key Vault resource builder where the connection string used to connect to this AzureRedisCacheResource will be stored.
/// A reference to the builder.
- public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder)
+ public static IResourceBuilder WithAccessKeyAuthentication(this IResourceBuilder builder, IResourceBuilder keyVaultBuilder)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(keyVaultBuilder);
diff --git a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs
index c3af1161d3f..a6d47daccd7 100644
--- a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs
+++ b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs
@@ -362,7 +362,7 @@ private static void ProcessAzureReferences(HashSet azureReferenc
return;
}
- if (value is IKeyVaultSecretReference keyVaultSecretReference)
+ if (value is IAzureKeyVaultSecretReference keyVaultSecretReference)
{
azureReferences.Add(keyVaultSecretReference.Resource);
return;
diff --git a/src/Aspire.Hosting.Azure/IKeyVaultResource.cs b/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs
similarity index 80%
rename from src/Aspire.Hosting.Azure/IKeyVaultResource.cs
rename to src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs
index 9226de80447..432b072dcf1 100644
--- a/src/Aspire.Hosting.Azure/IKeyVaultResource.cs
+++ b/src/Aspire.Hosting.Azure/IAzureKeyVaultResource.cs
@@ -8,7 +8,7 @@ namespace Aspire.Hosting.Azure;
///
/// Represents a resource that represents an Azure Key Vault.
///
-public interface IKeyVaultResource : IResource, IAzureResource
+public interface IAzureKeyVaultResource : IResource, IAzureResource
{
///
/// Gets the output reference that represents the vault uri for the Azure Key Vault resource.
@@ -23,12 +23,12 @@ public interface IKeyVaultResource : IResource, IAzureResource
///
/// Gets or sets the secret resolver function used to resolve secrets at runtime.
///
- Func>? SecretResolver { get; set; }
+ Func>? SecretResolver { get; set; }
///
/// Gets a secret reference for the specified secret name.
///
/// The name of the secret.
/// A reference to the secret.
- IKeyVaultSecretReference GetSecretReference(string secretName);
+ IAzureKeyVaultSecretReference GetSecretReference(string secretName);
}
diff --git a/src/Aspire.Hosting.Azure/IKeyVaultSecretReference.cs b/src/Aspire.Hosting.Azure/IAzureKeyVaultSecretReference.cs
similarity index 78%
rename from src/Aspire.Hosting.Azure/IKeyVaultSecretReference.cs
rename to src/Aspire.Hosting.Azure/IAzureKeyVaultSecretReference.cs
index 70f1cb30d58..b0a33b5a536 100644
--- a/src/Aspire.Hosting.Azure/IKeyVaultSecretReference.cs
+++ b/src/Aspire.Hosting.Azure/IAzureKeyVaultSecretReference.cs
@@ -8,7 +8,7 @@ namespace Aspire.Hosting.Azure;
///
/// Represents a reference to a secret in an Azure Key Vault resource.
///
-public interface IKeyVaultSecretReference : IValueProvider, IManifestExpressionProvider
+public interface IAzureKeyVaultSecretReference : IValueProvider, IManifestExpressionProvider
{
///
/// Gets the name of the secret.
@@ -18,5 +18,5 @@ public interface IKeyVaultSecretReference : IValueProvider, IManifestExpressionP
///
/// Gets the Azure Key Vault resource.
///
- IKeyVaultResource Resource { get; }
-}
\ No newline at end of file
+ IAzureKeyVaultResource Resource { get; }
+}
diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs
index 12581f26de2..d4a0c90d5cb 100644
--- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs
+++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs
@@ -278,15 +278,15 @@ await notificationService.PublishUpdateAsync(resource, state =>
}
// Populate secret outputs from key vault (if any)
- if (resource is IKeyVaultResource kvr)
+ if (resource is IAzureKeyVaultResource kvr)
{
var vaultUri = resource.Outputs[kvr.VaultUriOutputReference.Name] as string ?? throw new InvalidOperationException($"{kvr.VaultUriOutputReference.Name} not found in outputs.");
// Set the client for resolving secrets at runtime
var client = new SecretClient(new(vaultUri), context.Credential);
- kvr.SecretResolver = async (secretName, ct) =>
+ kvr.SecretResolver = async (secretRef, ct) =>
{
- var secret = await client.GetSecretAsync(secretName, cancellationToken: ct).ConfigureAwait(false);
+ var secret = await client.GetSecretAsync(secretRef.SecretName, cancellationToken: ct).ConfigureAwait(false);
return secret.Value.Value;
};
}
diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
index fc4b463bd24..e2d22a4aedb 100644
--- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
+++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
@@ -419,7 +419,7 @@ public static IResourceBuilder WithCreationScript(this
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(script);
- builder.WithAnnotation(new CreationScriptAnnotation(script));
+ builder.WithAnnotation(new PostgresCreateDatabaseScriptAnnotation(script));
return builder;
}
@@ -493,7 +493,7 @@ private static string WritePgAdminServerJson(IEnumerable
private static async Task CreateDatabaseAsync(NpgsqlConnection npgsqlConnection, PostgresDatabaseResource npgsqlDatabase, IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
- var scriptAnnotation = npgsqlDatabase.Annotations.OfType().LastOrDefault();
+ var scriptAnnotation = npgsqlDatabase.Annotations.OfType().LastOrDefault();
try
{
diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresCreateDatabaseScriptAnnotation.cs b/src/Aspire.Hosting.PostgreSQL/PostgresCreateDatabaseScriptAnnotation.cs
new file mode 100644
index 00000000000..54e64354d71
--- /dev/null
+++ b/src/Aspire.Hosting.PostgreSQL/PostgresCreateDatabaseScriptAnnotation.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.Postgres;
+
+///
+/// Represents an annotation for defining a script to create a database in PostgreSQL.
+///
+internal sealed class PostgresCreateDatabaseScriptAnnotation : IResourceAnnotation
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The script used to create the database.
+ public PostgresCreateDatabaseScriptAnnotation(string script)
+ {
+ ArgumentNullException.ThrowIfNull(script);
+ Script = script;
+ }
+
+ ///
+ /// Gets the script used to create the database.
+ ///
+ public string Script { get; }
+}
diff --git a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
index 977ea4c7cd3..a8271f79279 100644
--- a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
+++ b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
@@ -195,7 +195,7 @@ public static IResourceBuilder WithCreationScript(thi
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(script);
- builder.WithAnnotation(new CreationScriptAnnotation(script));
+ builder.WithAnnotation(new SqlServerCreateDatabaseScriptAnnotation(script));
return builder;
}
@@ -204,7 +204,7 @@ private static async Task CreateDatabaseAsync(SqlConnection sqlConnection, SqlSe
{
try
{
- var scriptAnnotation = sqlDatabase.Annotations.OfType().LastOrDefault();
+ var scriptAnnotation = sqlDatabase.Annotations.OfType().LastOrDefault();
if (scriptAnnotation?.Script == null)
{
diff --git a/src/Aspire.Hosting.SqlServer/SqlServerCreateDatabaseScriptAnnotation.cs b/src/Aspire.Hosting.SqlServer/SqlServerCreateDatabaseScriptAnnotation.cs
new file mode 100644
index 00000000000..7591d8fe97c
--- /dev/null
+++ b/src/Aspire.Hosting.SqlServer/SqlServerCreateDatabaseScriptAnnotation.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting;
+
+///
+/// Represents an annotation for defining a script to create a database in SQL Server.
+///
+internal sealed class SqlServerCreateDatabaseScriptAnnotation : IResourceAnnotation
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The script used to create the database.
+ public SqlServerCreateDatabaseScriptAnnotation(string script)
+ {
+ ArgumentNullException.ThrowIfNull(script);
+ Script = script;
+ }
+
+ ///
+ /// Gets the script used to create the database.
+ ///
+ public string Script { get; }
+}
diff --git a/src/Aspire.Hosting/ApplicationModel/CreationScriptAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/CreationScriptAnnotation.cs
deleted file mode 100644
index f66abcecf83..00000000000
--- a/src/Aspire.Hosting/ApplicationModel/CreationScriptAnnotation.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Aspire.Hosting.ApplicationModel;
-
-///
-/// Represents an annotation for defining a script to create a resource.
-///
-public sealed class CreationScriptAnnotation : IResourceAnnotation
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The script used to create the resource.
- public CreationScriptAnnotation(string script)
- {
- ArgumentNullException.ThrowIfNull(script);
- Script = script;
- }
-
- ///
- /// Gets the script used to create the resource.
- ///
- public string Script { get; }
-}
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs
index b25ccd148fe..5cd01f6b256 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net.Sockets;
+using System.Runtime.CompilerServices;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
@@ -237,6 +238,24 @@ public async Task AddAzureCosmosDBEmulator()
Assert.Equal(cs, await ((IResourceWithConnectionString)cosmos.Resource).GetConnectionStringAsync());
}
+ [Fact]
+ public async Task AddAzureCosmosDB_WithAccessKeyAuthentication_NoKeyVaultWithEmulator()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ builder.AddAzureCosmosDB("cosmos").WithAccessKeyAuthentication().RunAsEmulator();
+
+#pragma warning disable ASPIRECOSMOSDB001
+ builder.AddAzureCosmosDB("cosmos2").WithAccessKeyAuthentication().RunAsPreviewEmulator();
+#pragma warning restore ASPIRECOSMOSDB001
+
+ var app = builder.Build();
+ var model = app.Services.GetRequiredService();
+ await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
+
+ Assert.Empty(model.Resources.OfType());
+ }
+
[Theory]
[InlineData(null)]
[InlineData("mykeyvault")]
@@ -264,16 +283,24 @@ public async Task AddAzureCosmosDBViaRunMode_WithAccessKeyAuthentication(string?
var db = cosmos.AddCosmosDatabase("db", databaseName: "mydatabase");
db.AddContainer("container", "mypartitionkeypath", containerName: "mycontainer");
- var kv = builder.CreateResourceBuilder(kvName);
+ var app = builder.Build();
+
+ await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
+
+ var model = app.Services.GetRequiredService();
+
+ var kv = model.Resources.OfType().Single();
+
+ Assert.Equal(kvName, kv.Name);
var secrets = new Dictionary
{
["connectionstrings--cosmos"] = "mycosmosconnectionstring"
};
- kv.Resource.SecretResolver = (name, _) =>
+ kv.SecretResolver = (secretRef, _) =>
{
- if (!secrets.TryGetValue(name, out var value))
+ if (!secrets.TryGetValue(secretRef.SecretName, out var value))
{
return Task.FromResult(null);
}
@@ -533,9 +560,9 @@ public async Task AddAzureCosmosDBViaPublishMode_WithAccessKeyAuthentication(str
["connectionstrings--cosmos"] = "mycosmosconnectionstring"
};
- kv.Resource.SecretResolver = (name, _) =>
+ kv.Resource.SecretResolver = (secretRef, _) =>
{
- if (!secrets.TryGetValue(name, out var value))
+ if (!secrets.TryGetValue(secretRef.SecretName, out var value))
{
return Task.FromResult(null);
}
@@ -3130,6 +3157,9 @@ public async Task InfrastructureCanBeMutatedAfterCreation()
Assert.Equal(expectedBicep, bicep);
}
+ [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
+ private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
+
private sealed class ProjectA : IProjectMetadata
{
public string ProjectPath => "projectA";
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs
index 7f7e2d2e18a..4c5475efbec 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs
@@ -106,16 +106,26 @@ public void AzureCosmosDBHasCorrectConnectionStrings_ForAccountEndpoint()
Assert.Equal("AccountEndpoint={cosmos.outputs.connectionString};Database=db1;Container=container1", container1.Resource.ConnectionStringExpression.ValueExpression);
}
- [Fact]
- public void AzureCosmosDBHasCorrectConnectionStrings()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AzureCosmosDBHasCorrectConnectionStrings(bool useAccessKeyAuth)
{
using var builder = TestDistributedApplicationBuilder.Create();
var cosmos = builder.AddAzureCosmosDB("cosmos").RunAsEmulator();
+ if (useAccessKeyAuth)
+ {
+ cosmos.WithAccessKeyAuthentication();
+ }
var db1 = cosmos.AddCosmosDatabase("db1");
var container1 = db1.AddContainer("container1", "id");
var cosmos1 = builder.AddAzureCosmosDB("cosmos1").RunAsEmulator();
+ if (useAccessKeyAuth)
+ {
+ cosmos1.WithAccessKeyAuthentication();
+ }
var db2 = cosmos1.AddCosmosDatabase("db2", "db");
var container2 = db2.AddContainer("container2", "id", "container");
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs
index e8c90e020b6..cc011572937 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs
@@ -145,6 +145,20 @@ param principalName string
Assert.Equal(expectedBicep, postgresRolesManifest.BicepText);
}
+ [Fact]
+ public async Task AddAzurePostgresFlexibleServer_WithPasswordAuthentication_NoKeyVaultWithContainer()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ builder.AddAzurePostgresFlexibleServer("pg").WithPasswordAuthentication().RunAsContainer();
+
+ var app = builder.Build();
+ var model = app.Services.GetRequiredService();
+ await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
+
+ Assert.Empty(model.Resources.OfType());
+ }
+
[Theory]
[InlineData(true, true, null)]
[InlineData(true, true, "mykeyvault")]
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs
index 53dfe5b9770..bc3d6481fda 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs
@@ -99,6 +99,20 @@ param principalName string
Assert.Equal(expectedBicep, redisRolesManifest.BicepText);
}
+ [Fact]
+ public async Task AddAzureRedis_WithAccessKeyAuthentication_NoKeyVaultWithContainer()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ builder.AddAzureRedis("redis").WithAccessKeyAuthentication().RunAsContainer();
+
+ var app = builder.Build();
+ var model = app.Services.GetRequiredService();
+ await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
+
+ Assert.Empty(model.Resources.OfType());
+ }
+
[Theory]
[InlineData(null)]
[InlineData("mykeyvault")]