#pragma warning disable ASPIREPIPELINES002
#pragma warning disable ASPIREUSERSECRETS001
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Backchannel;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Resources;
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting;
/// <summary>
/// Handles processing of parameter resources during application orchestration.
/// </summary>
public sealed class ParameterProcessor(
ResourceNotificationService notificationService,
ResourceLoggerService loggerService,
IInteractionService interactionService,
ILogger<ParameterProcessor> logger,
DistributedApplicationExecutionContext executionContext,
IDeploymentStateManager deploymentStateManager,
IUserSecretsManager userSecretsManager)
{
internal const string SaveToUserSecretsName = "SaveToUserSecrets";
internal const string DeleteFromUserSecretsName = "DeleteFromUserSecrets";
internal const string SetParameterValueName = "Value";
private readonly List<ParameterResource> _unresolvedParameters = [];
private readonly object _resolutionTaskLock = new();
private CancellationTokenSource? _allParametersResolvedCts;
private Task? _parameterResolutionTask;
/// <summary>
/// AppHost-scoped history that records resolved secret parameter values so <c>aspire describe</c>/<c>watch</c>
/// can redact them. Assigned by DI after construction (see <c>DistributedApplicationBuilder</c>) rather than
/// injected through the public constructor, to keep the public API surface unchanged for backporting. Null when
/// the processor is created outside the AppHost container (e.g. in unit tests), in which case recording is a
/// no-op.
/// </summary>
internal SecretRedactionHistory? SecretRedactionHistory { get; set; }
/// <summary>
/// Initializes parameter resources and handles unresolved parameters if interaction service is available.
/// </summary>
/// <param name="parameterResources">The parameter resources to initialize.</param>
/// <param name="waitForResolution">Whether to wait for all parameters to be resolved before completing the returned Task.</param>
/// <returns>A task that completes when all parameters are resolved (if waitForResolution is true) or when initialization is complete.</returns>
public async Task InitializeParametersAsync(IEnumerable<ParameterResource> parameterResources, bool waitForResolution = false)
{
// Initialize all parameter resources by setting their WaitForValueTcs.
// This allows them to be processed asynchronously later.
foreach (var parameterResource in parameterResources)
{
parameterResource.WaitForValueTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await ProcessParameterAsync(parameterResource).ConfigureAwait(false);
}
// If interaction service is available, we can handle unresolved parameters.
// This will allow the user to provide values for parameters that could not be initialized.
if (interactionService.IsAvailable && _unresolvedParameters.Count > 0)
{
// Start the loop that will allow the user to specify values for unresolved parameters.
var task = EnsureParameterResolutionTaskRunningAsync();
if (waitForResolution)
{
await task.ConfigureAwait(false);
}
}
}
private Task EnsureParameterResolutionTaskRunningAsync()
{
lock (_resolutionTaskLock)
{
if (_parameterResolutionTask is null || _parameterResolutionTask.IsCompleted)
{
var cts = new CancellationTokenSource();
_allParametersResolvedCts = cts;
_parameterResolutionTask = Task.Run(async () =>
{
try
{
await HandleUnresolvedParametersAsync(_unresolvedParameters, cts.Token).ConfigureAwait(false);
logger.LogDebug("All unresolved parameters have been handled successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to handle unresolved parameters.");
}
});
}
return _parameterResolutionTask;
}
}
/// <summary>
/// Initializes parameter resources by collecting dependent parameters from the distributed application model
/// and handles unresolved parameters if interaction service is available.
/// </summary>
/// <param name="model">The distributed application model to collect parameters from.</param>
/// <param name="waitForResolution">Whether to wait for all parameters to be resolved before completing the returned Task.</param>
/// <param name="cancellationToken">The cancellation token to observe while waiting for parameters to be resolved.</param>
/// <returns>A task that completes when all parameters are resolved (if waitForResolution is true) or when initialization is complete.</returns>
public async Task InitializeParametersAsync(DistributedApplicationModel model, bool waitForResolution = false, CancellationToken cancellationToken = default)
{
var referencedParameters = new Dictionary<string, ParameterResource>();
await CollectDependentParameterResourcesAsync(model, referencedParameters, cancellationToken).ConfigureAwait(false);
// Combine explicit parameters with dependent parameters
var explicitParameters = model.Resources.OfType<ParameterResource>();
var dependentParameters = referencedParameters.Values.Where(p => !explicitParameters.Contains(p));
var allParameters = explicitParameters.Concat(dependentParameters).ToList();
if (allParameters.Any())
{
await InitializeParametersAsync(allParameters, waitForResolution).ConfigureAwait(false);
}
// In publish mode, save all parameter values at the end
if (executionContext.IsPublishMode && allParameters.Any())
{
await SaveParametersToDeploymentStateAsync(allParameters, cancellationToken).ConfigureAwait(false);
}
}
private async Task CollectDependentParameterResourcesAsync(DistributedApplicationModel model, Dictionary<string, ParameterResource> referencedParameters, CancellationToken cancellationToken)
{
foreach (var resource in model.Resources)
{
if (resource.IsExcludedFromPublish())
{
continue;
}
var dependencies = await resource.GetResourceDependenciesAsync(executionContext, ResourceDependencyDiscoveryMode.Recursive, cancellationToken).ConfigureAwait(false);
foreach (var parameter in dependencies.OfType<ParameterResource>())
{
referencedParameters.TryAdd(parameter.Name, parameter);
}
}
}
private async Task ProcessParameterAsync(ParameterResource parameterResource)
{
// Add the "Set parameter" command if the app is running and the interaction service is available.
// This command allows the user to set the parameter value at runtime.
if (executionContext.IsRunMode && interactionService.IsAvailable && !parameterResource.Annotations.OfType<ResourceCommandAnnotation>().Any(a => a.Name.Equals(KnownResourceCommands.SetParameterCommand, StringComparisons.CommandName)))
{
AddSetParameterCommand(parameterResource);
}
try
{
var value = parameterResource.ValueInternal ?? "";
await UpdateParameterStateAsync(parameterResource, value, KnownResourceStates.Running).ConfigureAwait(false);
parameterResource.WaitForValueTcs?.TrySetResult(value);
RecordSecretValueForRedaction(parameterResource, value);
}
catch (Exception ex)
{
// Missing parameter values throw a MissingParameterValueException.
if (interactionService.IsAvailable && ex is MissingParameterValueException)
{
// If interaction service is available, we can prompt the user to provide a value.
// Add the parameter to unresolved parameters list.
_unresolvedParameters.Add(parameterResource);
loggerService.GetLogger(parameterResource)
.LogWarning("Parameter resource {ResourceName} could not be initialized. Waiting for user input.", parameterResource.Name);
}
else
{
// If interaction service is not available, we log the error and set the state to error.
parameterResource.WaitForValueTcs?.TrySetException(ex);
loggerService.GetLogger(parameterResource)
.LogError(ex, "Failed to initialize parameter resource {ResourceName}.", parameterResource.Name);
}
var stateText = ex is MissingParameterValueException ?
KnownResourceStates.ValueMissing :
"Error initializing parameter";
// Use warning style for missing parameters to match the notification banner,
// and error style for actual initialization errors.
var stateStyle = ex is MissingParameterValueException ?
KnownResourceStateStyles.Warn :
KnownResourceStateStyles.Error;
await UpdateParameterStateAsync(parameterResource, ex.Message, new(stateText, stateStyle)).ConfigureAwait(false);
}
}
private void AddSetParameterCommand(ParameterResource parameterResource)
{
var valueInput = parameterResource.CreateInput(
SetParameterValueName,
required: true,
dynamicLoading: new InputLoadOptions
{
AlwaysLoadOnStart = true,
LoadCallback = context =>
{
if (context.Input.Value is not null)
{
return Task.CompletedTask;
}
try
{
var existingValue = parameterResource.ValueInternal;
if (!string.IsNullOrEmpty(existingValue))
{
context.Input.Value = existingValue;
}
}
catch (Exception)
{
// ValueInternal can throw when the parameter is unresolved; leave the input empty.
}
return Task.CompletedTask;
}
});
var saveInput = new InteractionInput
{
Name = SaveToUserSecretsName,
InputType = InputType.Boolean,
Label = InteractionStrings.ParametersInputsRememberLabel,
Description = !userSecretsManager.IsAvailable
? InteractionStrings.ParametersInputsRememberDescriptionNotConfigured
: InteractionStrings.ParametersInputsRememberDescriptionConfigured,
EnableDescriptionMarkdown = true,
Disabled = !userSecretsManager.IsAvailable,
DynamicLoading = new InputLoadOptions
{
AlwaysLoadOnStart = true,
DependsOnInputs = [SetParameterValueName],
LoadCallback = async context =>
{
if (context.Input.Value is null)
{
var parameterSection = await deploymentStateManager.AcquireSectionAsync(parameterResource.ConfigurationKey, context.CancellationToken).ConfigureAwait(false);
if (parameterSection.Data.Count > 0)
{
context.Input.Value = "true";
}
}
}
}
};
parameterResource.Annotations.Add(new ResourceCommandAnnotation(
name: KnownResourceCommands.SetParameterCommand,
displayName: CommandStrings.SetParameterName,
executeCommand: context => SetParameterCoreAsync(parameterResource, context.Arguments, context.CancellationToken),
updateState: _ => ResourceCommandState.Enabled,
displayDescription: CommandStrings.SetParameterDescription,
arguments: [valueInput, saveInput],
confirmationMessage: null,
iconName: "Key",
iconVariant: IconVariant.Regular,
isHighlighted: true));
var deleteFromSecretsInput = new InteractionInput
{
Name = DeleteFromUserSecretsName,
InputType = InputType.Boolean,
Label = InteractionStrings.ParametersInputsDeleteLabel,
Description = InteractionStrings.ParametersInputsDeleteDescription,
EnableDescriptionMarkdown = true,
Disabled = !userSecretsManager.IsAvailable,
DynamicLoading = new InputLoadOptions
{
AlwaysLoadOnStart = true,
LoadCallback = async context =>
{
if (!userSecretsManager.IsAvailable)
{
context.Input.Disabled = true;
return;
}
var parameterSection = await deploymentStateManager.AcquireSectionAsync(parameterResource.ConfigurationKey, context.CancellationToken).ConfigureAwait(false);
context.Input.Disabled = parameterSection.Data.Count == 0;
}
}
};
parameterResource.Annotations.Add(new ResourceCommandAnnotation(
name: KnownResourceCommands.DeleteParameterCommand,
displayName: CommandStrings.DeleteParameterName,
executeCommand: context => DeleteParameterCoreAsync(parameterResource, context.Arguments, context.CancellationToken),
updateState: _ => HasParameterValue(parameterResource) ? ResourceCommandState.Enabled : ResourceCommandState.Hidden,
displayDescription: CommandStrings.DeleteParameterDescription,
arguments: [deleteFromSecretsInput],
confirmationMessage: null,
iconName: "Delete",
iconVariant: IconVariant.Regular,
isHighlighted: true));
}
private static bool HasParameterValue(ParameterResource parameterResource)
{
try
{
var value = parameterResource.ValueInternal;
return !string.IsNullOrEmpty(value);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Prompts the user to set a value for a single parameter.
/// </summary>
/// <param name="parameterResource">The parameter resource to set the value for.</param>
/// <param name="cancellationToken">The cancellation token to observe while waiting for the interaction to complete.</param>
/// <returns>A task that completes when the user has set the value or canceled the interaction.</returns>
public async Task SetParameterAsync(ParameterResource parameterResource, CancellationToken cancellationToken = default)
{
var input = parameterResource.CreateInput(SetParameterValueName);
try
{
var existingValue = parameterResource.ValueInternal;
if (!string.IsNullOrEmpty(existingValue))
{
input.Value = existingValue;
}
}
catch (Exception)
{
// ValueInternal can throw when the parameter is unresolved; leave the input empty.
}
var parameterSection = await deploymentStateManager.AcquireSectionAsync(parameterResource.ConfigurationKey, cancellationToken).ConfigureAwait(false);
var hasSavedState = parameterSection.Data.Count > 0 && input.Value is not null;
var saveParameterInput = CreateSaveParameterInput(hasSavedState);
var result = await interactionService.PromptInputsAsync(
InteractionStrings.SetParameterTitle,
InteractionStrings.SetParameterMessage,
[input, saveParameterInput],
new InputsDialogInteractionOptions
{
PrimaryButtonText = InteractionStrings.ParametersInputsPrimaryButtonText,
ShowDismiss = true,
EnableMessageMarkdown = true,
},
cancellationToken).ConfigureAwait(false);
if (!result.Canceled)
{
await SetParameterCoreAsync(parameterResource, result.Data, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Prompts the user to delete a parameter value.
/// </summary>
/// <param name="parameterResource">The parameter resource to delete the value for.</param>
/// <param name="cancellationToken">The cancellation token to observe while waiting for the interaction to complete.</param>
/// <returns>A task that completes when the user has deleted the value or canceled the interaction.</returns>
public async Task DeleteParameterAsync(ParameterResource parameterResource, CancellationToken cancellationToken = default)
{
var parameterSection = await deploymentStateManager.AcquireSectionAsync(parameterResource.ConfigurationKey, cancellationToken).ConfigureAwait(false);
var hasSavedState = parameterSection.Data.Count > 0;
var message = string.Format(CultureInfo.CurrentCulture, InteractionStrings.DeleteParameterMessage, parameterResource.Name);
var deleteFromUserSecretsInput = new InteractionInput
{
Name = DeleteFromUserSecretsName,
InputType = InputType.Boolean,
Label = InteractionStrings.ParametersInputsDeleteLabel,
Description = InteractionStrings.ParametersInputsDeleteDescription,
EnableDescriptionMarkdown = true
};
var inputs = hasSavedState ? [deleteFromUserSecretsInput] : Array.Empty<InteractionInput>();
var result = await interactionService.PromptInputsAsync(
InteractionStrings.DeleteParameterTitle,
message,
inputs,
new InputsDialogInteractionOptions
{
PrimaryButtonText = InteractionStrings.DeleteParameterPrimaryButtonText,
ShowDismiss = true,
EnableMessageMarkdown = true,
},
cancellationToken).ConfigureAwait(false);
if (result.Canceled)
{
return;
}
await DeleteParameterCoreAsync(parameterResource, result.Data, cancellationToken).ConfigureAwait(false);
}
private InteractionInput CreateSaveParameterInput(bool hasExistingValue)
{
return new InteractionInput
{
Name = SaveToUserSecretsName,
InputType = InputType.Boolean,
Label = InteractionStrings.ParametersInputsRememberLabel,
// Default to true if value already exists (was read from user secrets)
Value = hasExistingValue ? "true" : null,
Description = !userSecretsManager.IsAvailable
? InteractionStrings.ParametersInputsRememberDescriptionNotConfigured
: InteractionStrings.ParametersInputsRememberDescriptionConfigured,
EnableDescriptionMarkdown = true,
Disabled = !userSecretsManager.IsAvailable
};
}
internal async Task<ExecuteCommandResult> SetParameterCoreAsync(ParameterResource parameterResource, InteractionInputCollection arguments, CancellationToken cancellationToken)
{
var value = arguments.GetString(SetParameterValueName);
if (string.IsNullOrEmpty(value))
{
return CommandResults.Success();
}
var shouldSave = arguments[SaveToUserSecretsName].Value is { Length: > 0 } sv &&
bool.TryParse(sv, out var s) && s;
await ApplyParameterValueAsync(parameterResource, value, shouldSave, cancellationToken).ConfigureAwait(false);
OnParameterResolved(_unresolvedParameters, parameterResource);
return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceSetParameter, parameterResource.Name) };
}
internal async Task<ExecuteCommandResult> DeleteParameterCoreAsync(ParameterResource parameterResource, InteractionInputCollection arguments, CancellationToken cancellationToken)
{
try
{
var deleteFromUserSecrets = arguments.TryGetByName(DeleteFromUserSecretsName, out var deleteFromUserSecretsInput) &&
deleteFromUserSecretsInput.Value is { Length: > 0 } deleteInputValue &&
bool.TryParse(deleteInputValue, out var shouldDelete) && shouldDelete;
if (deleteFromUserSecrets)
{
var parameterSection = await deploymentStateManager.AcquireSectionAsync(parameterResource.ConfigurationKey, cancellationToken).ConfigureAwait(false);
parameterSection.Data.Clear();
await deploymentStateManager.DeleteSectionAsync(parameterSection, cancellationToken).ConfigureAwait(false);
logger.LogInformation("Parameter value deleted from deployment state for {ParameterName}.", parameterResource.Name);
loggerService.GetLogger(parameterResource)
.LogInformation("Parameter resource {ResourceName} value has been deleted from user secrets.", parameterResource.Name);
}
else
{
logger.LogInformation("Parameter value cleared for {ParameterName} (not deleted from user secrets).", parameterResource.Name);
loggerService.GetLogger(parameterResource)
.LogInformation("Parameter resource {ResourceName} value has been cleared.", parameterResource.Name);
}
// Add the parameter back to unresolved parameters
if (!_unresolvedParameters.Contains(parameterResource))
{
_unresolvedParameters.Add(parameterResource);
// Reset the WaitForValueTcs so the parameter can be resolved again
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
tcs.SetException(new MissingParameterValueException("Parameter value has been deleted."));
parameterResource.WaitForValueTcs = tcs;
// Update the parameter's state to show it's missing a value
await UpdateParameterStateAsync(parameterResource, "Parameter value has been deleted", new(KnownResourceStates.ValueMissing, KnownResourceStateStyles.Warn)).ConfigureAwait(false);
// Start the resolution task if it's not running
_ = EnsureParameterResolutionTaskRunningAsync();
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to delete parameter {ParameterName} from deployment state.", parameterResource.Name);
return CommandResults.Failure($"Failed to delete parameter '{parameterResource.Name}'.");
}
return new ExecuteCommandResult { Success = true, Message = string.Format(CultureInfo.InvariantCulture, CommandStrings.ResourceDeletedParameter, parameterResource.Name) };
}
private async Task ApplyParameterValueAsync(ParameterResource parameterResource, string inputValue, bool saveToDeploymentState, CancellationToken cancellationToken = default)
{
// Update the parameter resource with the new value.
// The parameter could already have a value set so recreate TCS in that situation.
if (parameterResource.WaitForValueTcs?.Task.IsCompleted ?? false)
{
parameterResource.WaitForValueTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
}
parameterResource.WaitForValueTcs?.TrySetResult(inputValue);
RecordSecretValueForRedaction(parameterResource, inputValue);
await UpdateParameterStateAsync(parameterResource, inputValue, KnownResourceStates.Running).ConfigureAwait(false);
// Log that the parameter has been resolved
loggerService.GetLogger(parameterResource)
.LogInformation("Parameter resource {ResourceName} has been resolved via user interaction.", parameterResource.Name);
// Save to deployment state if requested and in run mode
if (executionContext.IsRunMode && saveToDeploymentState)
{
try
{
var slot = await deploymentStateManager.AcquireSectionAsync(parameterResource.ConfigurationKey, cancellationToken).ConfigureAwait(false);
slot.SetValue(inputValue);
await deploymentStateManager.SaveSectionAsync(slot, cancellationToken).ConfigureAwait(false);
logger.LogInformation("Parameter value saved to deployment state for {ParameterName}.", parameterResource.Name);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to save parameter {ParameterName} to deployment state.", parameterResource.Name);
}
}
}
// Record a resolved secret value into the AppHost-scoped redaction history at the moment it is assigned or
// replaced, so `aspire describe`/`watch` redacts it even before any backchannel connection has peeked it. The
// describe path only observes a secret once a connection is open; a value assigned (and possibly replaced) before
// the first connection would otherwise be absent from the history and leak from a lagging snapshot
// (https://github.com/microsoft/aspire/issues/19241). No-op for non-secret parameters, empty values, or when no
// history is wired (e.g. unit tests that construct the processor directly).
private void RecordSecretValueForRedaction(ParameterResource parameterResource, string? value)
{
if (parameterResource.Secret && value is { Length: > 0 } && SecretRedactionHistory is { } history)
{
history.AddValues([value]);
}
}
// Internal for testing purposes - allows passing specific parameters to test.
internal async Task HandleUnresolvedParametersAsync(IList<ParameterResource> unresolvedParameters, CancellationToken allParametersResolvedToken)
{
var stateModified = false;
// This method will continue in a loop until all unresolved parameters are resolved.
while (unresolvedParameters.Count > 0)
{
var showNotification = executionContext.IsRunMode;
var showSaveToSecrets = executionContext.IsRunMode;
var proceedToInputs = true;
if (showNotification)
{
// First we show a notification that there are unresolved parameters.
var result = await interactionService.PromptNotificationAsync(
InteractionStrings.ParametersBarTitle,
InteractionStrings.ParametersBarMessage,
new NotificationInteractionOptions
{
Intent = MessageIntent.Warning,
PrimaryButtonText = InteractionStrings.ParametersBarPrimaryButtonText
},
allParametersResolvedToken).ConfigureAwait(false);
if (result.Canceled)
{
logger.LogDebug("Unresolved parameters notification was dismissed. The notification will not be shown again.");
// OnParameterResolved cancels this token after the last parameter is resolved. Convert that
// cancellation into successful task completion so waitForResolution callers remain blocked
// without surfacing an OperationCanceledException.
var allParametersResolved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var registration = allParametersResolvedToken.Register(static state => ((TaskCompletionSource)state!).TrySetResult(), allParametersResolved);
await allParametersResolved.Task.ConfigureAwait(false);
break;
}
proceedToInputs = result.Data;
}
if (proceedToInputs)
{
// Now we build up a new form base on the unresolved parameters.
var resourceInputs = new List<(ParameterResource ParameterResource, InteractionInput Input)>();
foreach (var parameter in unresolvedParameters)
{
// Create an input for each unresolved parameter.
var input = parameter.CreateInput();
resourceInputs.Add((parameter, input));
}
var inputs = resourceInputs.Select(i => i.Input).ToList();
InteractionInput? saveParameters = null;
if (showSaveToSecrets)
{
saveParameters = CreateSaveParameterInput(hasExistingValue: false);
inputs.Add(saveParameters);
}
var message = executionContext.IsPublishMode
? InteractionStrings.ParametersInputsMessagePublishMode
: InteractionStrings.ParametersInputsMessage;
var valuesPrompt = await interactionService.PromptInputsAsync(
InteractionStrings.ParametersInputsTitle,
message,
[.. inputs],
new InputsDialogInteractionOptions
{
PrimaryButtonText = InteractionStrings.ParametersInputsPrimaryButtonText,
ShowDismiss = true,
EnableMessageMarkdown = true,
},
allParametersResolvedToken).ConfigureAwait(false);
if (!valuesPrompt.Canceled)
{
var shouldSave = saveParameters?.Value is not null &&
bool.TryParse(saveParameters.Value, out var saveToDeploymentState) && saveToDeploymentState;
// Iterate through the unresolved parameters and set their values based on user input.
for (var i = resourceInputs.Count - 1; i >= 0; i--)
{
var (parameter, input) = (resourceInputs[i].ParameterResource, resourceInputs[i].Input);
var inputValue = input.Value;
if (string.IsNullOrEmpty(inputValue))
{
// If the input value is null, we skip this parameter.
continue;
}
await ApplyParameterValueAsync(parameter, inputValue, shouldSave, allParametersResolvedToken).ConfigureAwait(false);
if (shouldSave)
{
stateModified = true;
}
// Remove the parameter from unresolved parameters list.
OnParameterResolved(unresolvedParameters, parameter);
}
}
}
}
if (stateModified)
{
logger.LogInformation("Parameter values saved to deployment state.");
}
}
private void OnParameterResolved(IList<ParameterResource> unresolvedParameters, ParameterResource parameter)
{
unresolvedParameters.Remove(parameter);
if (unresolvedParameters.Count == 0)
{
_allParametersResolvedCts?.Cancel();
}
}
private async Task SaveParametersToDeploymentStateAsync(IEnumerable<ParameterResource> parameters, CancellationToken cancellationToken)
{
var savedCount = 0;
foreach (var parameter in parameters)
{
try
{
var value = await parameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(value))
{
var slot = await deploymentStateManager.AcquireSectionAsync(parameter.ConfigurationKey, cancellationToken).ConfigureAwait(false);
slot.SetValue(value);
await deploymentStateManager.SaveSectionAsync(slot, cancellationToken).ConfigureAwait(false);
savedCount++;
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to save parameter {ParameterName} to deployment state.", parameter.Name);
}
}
if (savedCount > 0)
{
logger.LogInformation("{SavedCount} parameter values saved to deployment state.", savedCount);
}
}
private async Task UpdateParameterStateAsync(ParameterResource parameterResource, string value, ResourceStateSnapshot? state)
{
await notificationService.PublishUpdateAsync(parameterResource, s =>
{
return s with
{
Properties = s.Properties.SetResourcePropertyRange([parameterResource.CreateValueSnapshotProperty(value)]),
State = state
};
}).ConfigureAwait(false);
}
}