File: ApplicationModel\ExecutionConfigurationGathererContext.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.ApplicationModel;
 
internal class ExecutionConfigurationGathererContext : IExecutionConfigurationGathererContext
{
    /// <inheritdoc/>
    public List<object> Arguments { get; } = new();
 
    /// <inheritdoc/>
    public Dictionary<string, object> EnvironmentVariables { get; } = new();
 
    /// <summary>
    /// Additional configuration data collected during gathering.
    /// </summary>
    internal HashSet<IExecutionConfigurationData> AdditionalConfigurationData { get; } = new();
 
    /// <inheritdoc/>
    public void AddAdditionalData(IExecutionConfigurationData metadata)
    {
        AdditionalConfigurationData.Add(metadata);
    }
 
    /// <summary>
    /// Resolves the actual <see cref="IExecutionConfigurationResult"/> from the gatherer context.
    /// </summary>
    /// <param name="resource">The resource for which the configuration is being resolved.</param>
    /// <param name="resourceLogger">The logger associated with the resource.</param>
    /// <param name="executionContext">The execution context of the distributed application.</param>
    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
    /// <returns>
    /// A task that represents the asynchronous operation. The task result contains the resolved resource configuration.
    /// </returns>
    internal async Task<IExecutionConfigurationResult> ResolveAsync(
        IResource resource,
        ILogger resourceLogger,
        DistributedApplicationExecutionContext executionContext,
        CancellationToken cancellationToken = default)
    {
        HashSet<object> references = new();
        var launchToolArgumentsData = AdditionalConfigurationData.OfType<UnresolvedLaunchToolArgumentsData>().FirstOrDefault();
        var argumentCapacity = Arguments.Count + (launchToolArgumentsData?.Arguments.Length ?? 0);
        List<(object Unprocessed, string Value, bool IsSensitive)> resolvedArguments = new(argumentCapacity);
        Dictionary<string, (object Unprocessed, string Value)> resolvedEnvironmentVariables = new(EnvironmentVariables.Count);
        List<Exception> exceptions = new();
        var resolvedLaunchToolArgumentCount = 0;
 
        if (launchToolArgumentsData is not null)
        {
            await ResolveArgumentsAsync(launchToolArgumentsData.Arguments, areLaunchToolArguments: true).ConfigureAwait(false);
        }
 
        await ResolveArgumentsAsync(Arguments, areLaunchToolArguments: false).ConfigureAwait(false);
 
        foreach (var kvp in EnvironmentVariables)
        {
            try
            {
                var resolvedValue = await resource.ResolveValueAsync(executionContext, resourceLogger, kvp.Value, null, cancellationToken).ConfigureAwait(false);
                if (resolvedValue?.Value is not null)
                {
                    resolvedEnvironmentVariables[kvp.Key] = (kvp.Value, resolvedValue.Value);
                    if (kvp.Value is IValueProvider or IManifestExpressionProvider)
                    {
                        references.Add(kvp.Value);
                    }
                }
            }
            catch (Exception ex)
            {
                resourceLogger.LogError(ex, "Failed to resolve environment variable '{EnvironmentVariable}' for resource '{ResourceName}'. A dependency may have failed to start.", kvp.Key, resource.Name);
                exceptions.Add(ex);
            }
        }
 
        var resolvedAdditionalConfigurationData = AdditionalConfigurationData;
        if (launchToolArgumentsData is not null)
        {
            resolvedAdditionalConfigurationData = AdditionalConfigurationData
                .Where(data => data is not UnresolvedLaunchToolArgumentsData && data is not LaunchToolArgumentsData)
                .ToHashSet();
 
            resolvedAdditionalConfigurationData.Add(new LaunchToolArgumentsData(resolvedLaunchToolArgumentCount, launchToolArgumentsData.ShowInCommandLine));
        }
 
        return new ExecutionConfigurationResult
        {
            References = references,
            ArgumentsWithUnprocessed = resolvedArguments,
            EnvironmentVariablesWithUnprocessed = resolvedEnvironmentVariables,
            AdditionalConfigurationData = resolvedAdditionalConfigurationData,
            Exception = exceptions.Count == 0 ? null : new AggregateException("One or more errors occurred while resolving resource configuration.", exceptions)
        };
 
        async Task ResolveArgumentsAsync(IEnumerable<object> arguments, bool areLaunchToolArguments)
        {
            foreach (var argument in arguments)
            {
                try
                {
                    var resolvedValue = await resource.ResolveValueAsync(executionContext, resourceLogger, argument, null, cancellationToken).ConfigureAwait(false);
                    if (resolvedValue?.Value is not null)
                    {
                        resolvedArguments.Add((argument, resolvedValue.Value, resolvedValue.IsSensitive));
                        if (areLaunchToolArguments)
                        {
                            // Resolution drops null values. Count only launch tool values that survived so a missing
                            // prefix value cannot make consumers treat the first ordinary argument as part of the prefix.
                            resolvedLaunchToolArgumentCount++;
                        }
 
                        if (argument is IValueProvider or IManifestExpressionProvider)
                        {
                            references.Add(argument);
                        }
                    }
                }
                catch (Exception ex)
                {
                    resourceLogger.LogError(ex, "Failed to resolve argument for resource '{ResourceName}'. A dependency may have failed to start.", resource.Name);
                    exceptions.Add(ex);
                }
            }
        }
    }
}