File: ApplicationModel\ResourceExtensions.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.
 
#pragma warning disable ASPIREPERSISTENCE001 // Persistence annotation APIs are experimental.
 
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Aspire.Dashboard.Model;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
 
#pragma warning disable ASPIRECOMPUTE003
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Provides extension methods for the <see cref="IResource"/> interface.
/// </summary>
public static class ResourceExtensions
{
    /// <summary>
    /// Attempts to get the last annotation of the specified type from the resource.
    /// </summary>
    /// <typeparam name="T">The type of the annotation to get.</typeparam>
    /// <param name="resource">The resource to get the annotation from.</param>
    /// <param name="annotation">When this method returns, contains the last annotation of the specified type from the resource, if found; otherwise, the default value for <typeparamref name="T"/>.</param>
    /// <returns><see langword="true"/> if the last annotation of the specified type was found in the resource; otherwise, <see langword="false"/>.</returns>
    [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")]
    public static bool TryGetLastAnnotation<T>(this IResource resource, [NotNullWhen(true)] out T? annotation) where T : IResourceAnnotation
    {
        var lastAnnotation = resource.Annotations.OfType<T>().LastOrDefault();
 
        if (lastAnnotation is not null)
        {
            annotation = lastAnnotation;
            return true;
        }
        else
        {
            annotation = default;
            return false;
        }
    }
 
    /// <summary>
    /// Attempts to retrieve all annotations of the specified type from the given resource.
    /// </summary>
    /// <typeparam name="T">The type of annotation to retrieve.</typeparam>
    /// <param name="resource">The resource to retrieve annotations from.</param>
    /// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, <see langword="null"/>.</param>
    /// <returns><see langword="true"/> if annotations of the specified type were found; otherwise, <see langword="false"/>.</returns>
    [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")]
    public static bool TryGetAnnotationsOfType<T>(this IResource resource, [NotNullWhen(true)] out IEnumerable<T>? result) where T : IResourceAnnotation
    {
        var matchingTypeAnnotations = resource.Annotations.OfType<T>().ToArray();
 
        if (matchingTypeAnnotations.Length > 0)
        {
            result = matchingTypeAnnotations;
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }
 
    /// <summary>
    /// Gets whether <paramref name="resource"/> has an annotation of type <typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">The type of annotation to retrieve.</typeparam>
    /// <param name="resource">The resource to retrieve annotations from.</param>
    /// <returns><see langword="true"/> if an annotation of the specified type was found; otherwise, <see langword="false"/>.</returns>
    [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")]
    public static bool HasAnnotationOfType<T>(this IResource resource) where T : IResourceAnnotation
    {
        return resource.Annotations.Any(a => a is T);
    }
 
    /// <summary>
    /// Attempts to retrieve all annotations of the specified type from the given resource including from parents.
    /// </summary>
    /// <typeparam name="T">The type of annotation to retrieve.</typeparam>
    /// <param name="resource">The resource to retrieve annotations from.</param>
    /// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, <see langword="null"/>.</param>
    /// <returns><see langword="true"/> if annotations of the specified type were found; otherwise, <see langword="false"/>.</returns>
    [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")]
    public static bool TryGetAnnotationsIncludingAncestorsOfType<T>(this IResource resource, [NotNullWhen(true)] out IEnumerable<T>? result) where T : IResourceAnnotation
    {
        if (resource is IResourceWithParent)
        {
            List<T>? annotations = null;
 
            while (true)
            {
                foreach (var annotation in resource.Annotations.OfType<T>())
                {
                    annotations ??= [];
                    annotations.Add(annotation);
                }
 
                if (resource is IResourceWithParent child)
                {
                    resource = child.Parent;
                }
                else
                {
                    break;
                }
            }
 
            result = annotations;
            return annotations is not null;
        }
 
        return TryGetAnnotationsOfType(resource, out result);
    }
 
    /// <summary>
    /// Gets whether <paramref name="resource"/> or its ancestors have an annotation of type <typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">The type of annotation to retrieve.</typeparam>
    /// <param name="resource">The resource to retrieve annotations from.</param>
    /// <returns><see langword="true"/> if an annotation of the specified type was found; otherwise, <see langword="false"/>.</returns>
    [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")]
    public static bool HasAnnotationIncludingAncestorsOfType<T>(this IResource resource) where T : IResourceAnnotation
    {
        if (resource is IResourceWithParent)
        {
            while (true)
            {
                if (HasAnnotationOfType<T>(resource))
                {
                    return true;
                }
 
                if (resource is IResourceWithParent child)
                {
                    resource = child.Parent;
                }
                else
                {
                    break;
                }
            }
 
            return false;
        }
 
        return HasAnnotationOfType<T>(resource);
    }
 
    /// <summary>
    /// Attempts to get the environment variables from the given resource.
    /// </summary>
    /// <param name="resource">The resource to get the environment variables from.</param>
    /// <param name="environmentVariables">The environment variables retrieved from the resource, if any.</param>
    /// <returns>True if the environment variables were successfully retrieved, false otherwise.</returns>
    [AspireExportIgnore(Reason = "Environment callback inspection helper — not part of the ATS surface.")]
    public static bool TryGetEnvironmentVariables(this IResource resource, [NotNullWhen(true)] out IEnumerable<EnvironmentCallbackAnnotation>? environmentVariables)
    {
        return TryGetAnnotationsOfType(resource, out environmentVariables);
    }
 
    /// <summary>
    /// Get the environment variables from the given resource.
    /// </summary>
    /// <param name="resource">The resource to get the environment variables from.</param>
    /// <param name="applicationOperation">The context in which the AppHost is being executed.</param>
    /// <returns>The environment variables retrieved from the resource.</returns>
    /// <remarks>
    /// This method is useful when you want to make sure the environment variables are added properly to resources, mostly in test situations.
    /// This method has asynchronous behavior when <paramref name = "applicationOperation" /> is <see cref="DistributedApplicationOperation.Run"/>
    /// and environment variables were provided from <see cref="IValueProvider"/> otherwise it will be synchronous.
    /// <example>
    /// Using <see cref="GetEnvironmentVariableValuesAsync(IResourceWithEnvironment, DistributedApplicationOperation)"/> inside
    /// a unit test to validate environment variable values.
    /// <code>
    /// var builder = DistributedApplication.CreateBuilder();
    /// var container = builder.AddContainer("elasticsearch", "library/elasticsearch", "8.14.0")
    ///  .WithEnvironment("discovery.type", "single-node")
    ///  .WithEnvironment("xpack.security.enabled", "true");
    ///
    /// var env = await container.Resource.GetEnvironmentVariableValuesAsync();
    ///
    /// Assert.Collection(env,
    ///     env =>
    ///         {
    ///             Assert.Equal("discovery.type", env.Key);
    ///             Assert.Equal("single-node", env.Value);
    ///         },
    ///         env =>
    ///         {
    ///             Assert.Equal("xpack.security.enabled", env.Key);
    ///             Assert.Equal("true", env.Value);
    ///         });
    /// </code>
    /// </example>
    /// </remarks>
    [Obsolete($"Use {nameof(ExecutionConfigurationBuilder)} instead.")]
    public static async ValueTask<Dictionary<string, string>> GetEnvironmentVariableValuesAsync(this IResourceWithEnvironment resource,
            DistributedApplicationOperation applicationOperation = DistributedApplicationOperation.Run)
    {
        var executionConfiguration = await ExecutionConfigurationBuilder.Create(resource)
            .WithEnvironmentVariablesConfig()
            .BuildAsync(new(applicationOperation), NullLogger.Instance, CancellationToken.None).ConfigureAwait(false);
 
        return executionConfiguration.EnvironmentVariables.ToDictionary();
    }
 
    /// <summary>
    /// Get the arguments from the given resource.
    /// </summary>
    /// <param name="resource">The resource to get the arguments from.</param>
    /// <param name="applicationOperation">The context in which the AppHost is being executed.</param>
    /// <returns>The arguments retrieved from the resource.</returns>
    /// <remarks>
    /// This method is useful when you want to make sure the arguments are added properly to resources, mostly in test situations.
    /// This method has asynchronous behavior when <paramref name = "applicationOperation" /> is <see cref="DistributedApplicationOperation.Run"/>
    /// and arguments were provided from <see cref="IValueProvider"/> otherwise it will be synchronous.
    /// <example>
    /// Using <see cref="GetArgumentValuesAsync(IResourceWithArgs, DistributedApplicationOperation)"/> inside
    /// a unit test to validate argument values.
    /// <code>
    /// var builder = DistributedApplication.CreateBuilder();
    /// var container = builder.AddContainer("elasticsearch", "library/elasticsearch", "8.14.0")
    ///  .WithArgs("--discovery.type", "single-node")
    ///  .WithArgs("--xpack.security.enabled", "true");
    ///
    /// var args = await container.Resource.GetArgumentsAsync();
    ///
    /// Assert.Collection(args,
    ///     arg =>
    ///         {
    ///             Assert.Equal("--discovery.type", arg);
    ///         },
    ///         arg =>
    ///         {
    ///             Assert.Equal("--xpack.security.enabled", arg);
    ///         });
    /// </code>
    /// </example>
    /// </remarks>
    [Obsolete($"Use {nameof(ExecutionConfigurationBuilder)} instead.")]
    public static async ValueTask<string[]> GetArgumentValuesAsync(this IResourceWithArgs resource,
        DistributedApplicationOperation applicationOperation = DistributedApplicationOperation.Run)
    {
        var argumentConfiguration = await ExecutionConfigurationBuilder.Create(resource)
            .WithArgumentsConfig()
            .BuildAsync(new(applicationOperation), NullLogger.Instance, CancellationToken.None).ConfigureAwait(false);
 
        return argumentConfiguration.Arguments.Select(a => a.Value).ToArray();
    }
 
    /// <summary>
    /// Gathers argument values without resolving them or using cached callback results.
    /// </summary>
    /// <param name="resource">The resource to retrieve argument values for.</param>
    /// <param name="executionContext">The execution context used during the retrieval of argument values.</param>
    /// <param name="logger">The logger used for logging information or errors during the retrieval of argument values.</param>
    /// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
    /// <returns>A list of unprocessed argument values.</returns>
    internal static async ValueTask<List<object>> GatherArgumentValuesWithoutCachingAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        var args = new List<object>();
        if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var callbacks))
        {
            var context = new CommandLineArgsCallbackContext(args, resource, cancellationToken)
            {
                Logger = logger,
                ExecutionContext = executionContext
            };
 
            foreach (var callback in callbacks)
            {
                await callback.Callback(context).ConfigureAwait(false);
            }
        }
 
        var launchToolArgs = await GatherLaunchToolArgumentValuesAsync(
            resource,
            executionContext,
            logger,
            cacheAnnotationCallbackResult: false,
            peekCachedResultOnly: false,
            cancellationToken).ConfigureAwait(false);
        args.InsertRange(0, launchToolArgs);
 
        return args;
    }
 
    private static async ValueTask<IList<object>> GatherLaunchToolArgumentValuesAsync(
        IResource resource,
        DistributedApplicationExecutionContext executionContext,
        ILogger logger,
        bool cacheAnnotationCallbackResult,
        bool peekCachedResultOnly,
        CancellationToken cancellationToken)
    {
        // Launch tool arguments run against an isolated list and do not apply to containers, matching
        // ArgumentsExecutionConfigurationGatherer's composition of the effective command line.
        if (resource.IsContainer() ||
            !resource.TryGetLastAnnotation<LaunchToolArgsCallbackAnnotation>(out var annotation))
        {
            return [];
        }
 
        if (peekCachedResultOnly)
        {
            // Read-only discovery: never invoke the callback. Only surface a result DCP has already resolved
            // and cached; skip anything still in flight, faulted, or canceled.
            return annotation.AsCallbackAnnotation().TryGetCachedResult(out var cachedTask) && cachedTask!.IsCompletedSuccessfully
                ? cachedTask.Result
                : [];
        }
 
        var context = new CommandLineArgsCallbackContext([], resource, cancellationToken)
        {
            Logger = logger,
            ExecutionContext = executionContext
        };
 
        if (cacheAnnotationCallbackResult)
        {
            return await annotation.AsCallbackAnnotation().EvaluateOnceAsync(context).ConfigureAwait(false);
        }
 
        await annotation.Callback(context).ConfigureAwait(false);
 
        return context.Args;
    }
 
    /// <summary>
    /// Processes pre-gathered command-line argument values for the specified resource in the given execution context.
    /// </summary>
    /// <param name="resource">The resource for which the argument values are being processed.</param>
    /// <param name="executionContext">The execution context used during the processing of argument values.</param>
    /// <param name="arguments">The list of pre-gathered argument values to process.</param>
    /// <param name="processValue">A callback invoked for each argument value, providing the unprocessed value, processed string representation, any exception, and a sensitivity flag.</param>
    /// <param name="logger">The logger used for logging information or errors during the argument processing.</param>
    /// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    [Obsolete("Use ExecutionConfigurationBuilder instead.")]
    internal static async ValueTask ProcessGatheredArgumentValuesAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        List<object> arguments,
        // (unprocessed, processed, exception, isSensitive)
        Action<object?, string?, Exception?, bool> processValue,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        foreach (var a in arguments)
        {
            try
            {
                var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, a, null, cancellationToken).ConfigureAwait(false);
 
                if (resolvedValue?.Value != null)
                {
                    processValue(a, resolvedValue.Value, null, resolvedValue.IsSensitive);
                }
            }
            catch (Exception ex)
            {
                processValue(a, a.ToString(), ex, false);
            }
        }
    }
 
    /// <summary>
    /// Processes argument values for the specified resource in the given execution context.
    /// </summary>
    /// <param name="resource">The resource containing the argument values to process.</param>
    /// <param name="executionContext">The execution context used during the processing of argument values.</param>
    /// <param name="processValue">
    /// A callback invoked for each argument value. This action provides the unprocessed value, processed string representation,
    /// an exception if one occurs, and a boolean indicating the success of processing.
    /// </param>
    /// <param name="logger">The logger used for logging information or errors during the argument processing.</param>
    /// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    [Obsolete("Use ExecutionConfigurationBuilder instead.")]
    public static async ValueTask ProcessArgumentValuesAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        // (unprocessed, processed, exception, isSensitive)
        Action<object?, string?, Exception?, bool> processValue,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        var args = await GatherArgumentValuesWithoutCachingAsync(resource, executionContext, logger, cancellationToken).ConfigureAwait(false);
 
        await ProcessGatheredArgumentValuesAsync(resource, executionContext, args, processValue, logger, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Gather environment variable values, but do not resolve them. Used to allow multiple callbacks to
    /// contribute to the environment variable list before resolving.
    /// </summary>
    /// <param name="resource">The resource containing the environment variables to gather.</param>
    /// <param name="executionContext">The execution context used during the gathering of environment variables.</param>
    /// <param name="logger">The logger used for logging information or errors during the gathering process.</param>
    /// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
    /// <returns>A dictionary of unprocessed environment variable values.</returns>
    [Obsolete("Use ExecutionConfigurationBuilder instead.")]
    internal static async ValueTask<Dictionary<string, object>> GatherEnvironmentVariableValuesAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        var config = new Dictionary<string, object>();
        if (resource.TryGetEnvironmentVariables(out var callbacks))
        {
            var context = new EnvironmentCallbackContext(executionContext, resource, config, cancellationToken)
            {
                Logger = logger
            };
 
            foreach (var callback in callbacks)
            {
                await callback.Callback(context).ConfigureAwait(false);
            }
        }
 
        return config;
    }
 
    /// <summary>
    /// Processes pre-gathered environment variable values for the specified resource within the given execution context.
    /// </summary>
    /// <param name="resource">The resource for which the environment variables are being processed.</param>
    /// <param name="executionContext">The execution context used during the processing of environment variables.</param>
    /// <param name="environmentVariables">The pre-gathered environment variable values to be processed.</param>
    /// <param name="processValue">An action delegate invoked for each environment variable, providing the key, the unprocessed value, the processed value (if available), and any exception encountered during processing.</param>
    /// <param name="logger">The logger used to log any information or errors during the environment variables processing.</param>
    /// <param name="cancellationToken">A cancellation token to observe during the asynchronous operation.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Obsolete("Use ExecutionConfigurationBuilder instead.")]
    internal static async ValueTask ProcessGatheredEnvironmentVariableValuesAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        Dictionary<string, object> environmentVariables,
        Action<string, object?, string?, Exception?> processValue,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        foreach (var (key, expr) in environmentVariables)
        {
            try
            {
                var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, expr, key, cancellationToken).ConfigureAwait(false);
 
                if (resolvedValue?.Value is not null)
                {
                    processValue(key, expr, resolvedValue.Value, null);
                }
            }
            catch (Exception ex)
            {
                processValue(key, expr, expr?.ToString(), ex);
            }
        }
    }
 
    /// <summary>
    /// Processes environment variable values for the specified resource within the given execution context.
    /// </summary>
    /// <param name="resource">The resource from which the environment variables are retrieved and processed.</param>
    /// <param name="executionContext">The execution context to be used for processing the environment variables.</param>
    /// <param name="processValue">An action delegate invoked for each environment variable, providing the key, the unprocessed value, the processed value (if available), and any exception encountered during processing.</param>
    /// <param name="logger">The logger used to log any information or errors during the environment variables processing.</param>
    /// <param name="cancellationToken">A cancellation token to observe during the asynchronous operation.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    [Obsolete("Use ExecutionConfigurationBuilder instead.")]
    public static async ValueTask ProcessEnvironmentVariableValuesAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        Action<string, object?, string?, Exception?> processValue,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        var config = await GatherEnvironmentVariableValuesAsync(resource, executionContext, logger, cancellationToken).ConfigureAwait(false);
 
        await ProcessGatheredEnvironmentVariableValuesAsync(resource, executionContext, config, processValue, logger, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Processes all container build options callback annotations on a resource by invoking them in order.
    /// </summary>
    /// <param name="resource">The resource to process container build options for.</param>
    /// <param name="serviceProvider">The service provider for dependency injection.</param>
    /// <param name="logger">The logger used to log any information or errors during processing.</param>
    /// <param name="executionContext">
    /// The execution context to expose on the callback context. When <see langword="null"/> (the default),
    /// the execution context is resolved from <paramref name="serviceProvider"/>.
    /// </param>
    /// <param name="cancellationToken">A cancellation token to observe during the asynchronous operation.</param>
    /// <returns>A context object containing the accumulated container build options from all callbacks.</returns>
    [Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
    internal static async ValueTask<ContainerBuildOptionsCallbackContext> ProcessContainerBuildOptionsCallbackAsync(
        this IResource resource,
        IServiceProvider serviceProvider,
        ILogger logger,
        DistributedApplicationExecutionContext? executionContext = null,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(serviceProvider);
 
        var context = new ContainerBuildOptionsCallbackContext(
            resource,
            serviceProvider,
            logger,
            cancellationToken,
            executionContext ?? serviceProvider.GetRequiredService<DistributedApplicationExecutionContext>());
 
        if (resource.TryGetAnnotationsOfType<ContainerBuildOptionsCallbackAnnotation>(out var annotations))
        {
            foreach (var annotation in annotations)
            {
                await annotation.Callback(context).ConfigureAwait(false);
            }
        }
 
        return context;
    }
 
    /// <summary>
    /// Configures container build options for a compute resource using a callback.
    /// </summary>
    /// <typeparam name="T">The resource type.</typeparam>
    /// <param name="builder">The resource builder.</param>
    /// <param name="callback">A callback to configure container build options.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    [Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
    [AspireExportIgnore(Reason = "Polyglot AppHosts use the async callback overload.")]
    public static IResourceBuilder<T> WithContainerBuildOptions<T>(
        this IResourceBuilder<T> builder,
        Action<ContainerBuildOptionsCallbackContext> callback)
        where T : IResource, IComputeResource
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(callback);
 
        return builder.WithAnnotation(new ContainerBuildOptionsCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Append);
    }
 
    /// <summary>
    /// Configures container build options for a compute resource using an async callback.
    /// </summary>
    /// <typeparam name="T">The resource type.</typeparam>
    /// <param name="builder">The resource builder.</param>
    /// <param name="callback">An async callback to configure container build options.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    [Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
    [AspireExport]
    public static IResourceBuilder<T> WithContainerBuildOptions<T>(
        this IResourceBuilder<T> builder,
        Func<ContainerBuildOptionsCallbackContext, Task> callback)
        where T : IResource, IComputeResource
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(callback);
 
        return builder.WithAnnotation(new ContainerBuildOptionsCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Append);
    }
 
    internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resource)
    {
        return resource.IsContainer() ? KnownNetworkIdentifiers.DefaultAspireContainerNetwork : KnownNetworkIdentifiers.LocalhostNetwork;
    }
 
    internal static IEnumerable<NetworkIdentifier> GetSupportedNetworks(this IResource resource)
    {
        return resource.IsContainer() ? [KnownNetworkIdentifiers.DefaultAspireContainerNetwork, KnownNetworkIdentifiers.LocalhostNetwork] : [KnownNetworkIdentifiers.LocalhostNetwork];
    }
 
    internal static async ValueTask<ResolvedValue?> ResolveValueAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        ILogger logger,
        object? value,
        string? key = null,
        CancellationToken cancellationToken = default)
    {
        return (executionContext.Operation, value) switch
        {
            (_, string s) => new(s, false),
            (DistributedApplicationOperation.Run, IValueProvider provider) => await resource.GetValue(executionContext, key, provider, logger, cancellationToken).ConfigureAwait(false),
            (DistributedApplicationOperation.Run, IResourceBuilder<IResource> rb) when rb.Resource is IValueProvider provider => await resource.GetValue(executionContext, key, provider, logger, cancellationToken).ConfigureAwait(false),
            (DistributedApplicationOperation.Publish, IManifestExpressionProvider provider) => new(provider.ValueExpression, false),
            (DistributedApplicationOperation.Publish, IResourceBuilder<IResource> rb) when rb.Resource is IManifestExpressionProvider provider => new(provider.ValueExpression, false),
            (_, { } o) => new(o.ToString(), false),
            (_, null) => new(null, false),
        };
    }
 
    /// <summary>
    /// Gets a value indicating whether the resource is excluded from being published.
    /// </summary>
    /// <param name="resource">The resource to determine if it should be excluded from being published.</param>
    [AspireExportIgnore(Reason = "Manifest inspection helper — not part of the ATS surface.")]
    public static bool IsExcludedFromPublish(this IResource resource) =>
        resource.TryGetLastAnnotation<ManifestPublishingCallbackAnnotation>(out var lastAnnotation) && lastAnnotation == ManifestPublishingCallbackAnnotation.Ignore;
 
    internal static async ValueTask ProcessContainerRuntimeArgValues(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        Action<string?, Exception?> processValue,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        // Apply optional extra arguments to the container run command.
        if (resource.TryGetAnnotationsOfType<ContainerRuntimeArgsCallbackAnnotation>(out var runArgsCallback))
        {
            var args = new List<object>();
 
            var containerRunArgsContext = new ContainerRuntimeArgsCallbackContext(args, cancellationToken);
 
            foreach (var callback in runArgsCallback)
            {
                await callback.Callback(containerRunArgsContext).ConfigureAwait(false);
            }
 
            foreach (var arg in args)
            {
                try
                {
                    var value = arg switch
                    {
                        string s => s,
                        IValueProvider valueProvider => (await resource.GetValue(executionContext, key: null, valueProvider, logger, cancellationToken).ConfigureAwait(false))?.Value,
                        { } obj => obj.ToString(),
                        null => null
                    };
 
                    if (value is not null)
                    {
                        processValue(value, null);
                    }
                }
                catch (Exception ex)
                {
                    processValue(arg.ToString(), ex);
                }
            }
        }
    }
 
    private static async Task<ResolvedValue?> GetValue(this IResource resource, DistributedApplicationExecutionContext executionContext, string? key, IValueProvider valueProvider, ILogger logger, CancellationToken cancellationToken)
    {
        var task = ExpressionResolver.ResolveAsync(valueProvider, new ValueProviderContext() { ExecutionContext = executionContext, Caller = resource }, cancellationToken);
 
        if (!task.IsCompleted)
        {
            if (valueProvider is IResource providerResource)
            {
                if (key is null)
                {
                    logger.LogInformation("Waiting for value from resource '{ResourceName}'", providerResource.Name);
                }
                else
                {
                    logger.LogInformation("Waiting for value for environment variable value '{Name}' from resource '{ResourceName}'", key, providerResource.Name);
                }
            }
            else if (valueProvider is ConnectionStringReference { Resource: var cs })
            {
                logger.LogInformation("Waiting for value for connection string from resource '{ResourceName}'", cs.Name);
            }
            else if (TryGetEndpointReference(valueProvider, out var endpointReference))
            {
                logger.LogInformation(
                    "Waiting for endpoint '{EndpointName}' on resource '{ResourceName}' for the '{NetworkName}' network",
                    endpointReference.EndpointName,
                    endpointReference.Resource.Name,
                    endpointReference.ContextNetworkID?.Value);
            }
            else
            {
                if (key is null)
                {
                    logger.LogInformation("Waiting for value from {ValueProvider}.", valueProvider.ToString());
                }
                else
                {
                    logger.LogInformation("Waiting for value for environment variable value '{Name}' from {ValueProvider}.", key, valueProvider.ToString());
                }
            }
        }
 
        return await task.ConfigureAwait(false);
    }
 
    private static bool TryGetEndpointReference(IValueProvider valueProvider, [NotNullWhen(true)] out EndpointReference? endpointReference)
    {
        endpointReference = valueProvider switch
        {
            EndpointReference endpoint => endpoint,
            EndpointReferenceExpression { Endpoint: var endpoint } => endpoint,
            _ => null
        };
 
        return endpointReference is not null;
    }
 
    /// <summary>
    /// Attempts to get the container mounts for the specified resource.
    /// </summary>
    /// <param name="resource">The resource to get the volume mounts for.</param>
    /// <param name="volumeMounts">When this method returns, contains the volume mounts for the specified resource, if found; otherwise, <c>null</c>.</param>
    /// <returns><c>true</c> if the volume mounts were successfully retrieved; otherwise, <c>false</c>.</returns>
    [AspireExportIgnore(Reason = "Container mount inspection helper — not part of the ATS surface.")]
    public static bool TryGetContainerMounts(this IResource resource, [NotNullWhen(true)] out IEnumerable<ContainerMountAnnotation>? volumeMounts)
    {
        return TryGetAnnotationsOfType<ContainerMountAnnotation>(resource, out volumeMounts);
    }
 
    /// <summary>
    /// Attempts to retrieve the endpoints for the given resource.
    /// </summary>
    /// <param name="resource">The resource to retrieve the endpoints for.</param>
    /// <param name="endpoints">The endpoints for the given resource, if found.</param>
    /// <returns>True if the endpoints were found, false otherwise.</returns>
    [AspireExportIgnore(Reason = "Endpoint annotation inspection helper — not part of the ATS surface.")]
    public static bool TryGetEndpoints(this IResource resource, [NotNullWhen(true)] out IEnumerable<EndpointAnnotation>? endpoints)
    {
        return TryGetAnnotationsOfType(resource, out endpoints);
    }
 
    /// <summary>
    /// Attempts to retrieve the URLs for the given resource.
    /// </summary>
    /// <param name="resource">The resource to retrieve the URLs for.</param>
    /// <param name="urls">The URLs for the given resource, if found.</param>
    /// <returns>True if the URLs were found, false otherwise.</returns>
    [AspireExportIgnore(Reason = "URL annotation inspection helper — not part of the ATS surface.")]
    public static bool TryGetUrls(this IResource resource, [NotNullWhen(true)] out IEnumerable<ResourceUrlAnnotation>? urls)
    {
        return TryGetAnnotationsOfType(resource, out urls);
    }
 
    /// <summary>
    /// Gets references to all endpoints for the specified resource.
    /// </summary>
    /// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
    /// <returns>An enumeration of <see cref="EndpointReference"/> based on the <see cref="EndpointAnnotation"/> annotations from the resources' <see cref="IResource.Annotations"/> collection.</returns>
    [AspireExportIgnore(Reason = "Resource handle endpoint enumeration is not part of the ATS surface; use builder-based endpoint exports instead.")]
    public static IEnumerable<EndpointReference> GetEndpoints(this IResourceWithEndpoints resource)
    {
        if (TryGetAnnotationsOfType<EndpointAnnotation>(resource, out var endpoints))
        {
            return endpoints.Select(e => new EndpointReference(resource, e));
        }
 
        return [];
    }
 
    /// <summary>
    /// Gets references to all endpoints for the specified resource.
    /// </summary>
    /// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
    /// <param name="contextNetworkId">The ID of the network that serves as the context context for the endpoint references.</param>
    /// <returns>An enumeration of <see cref="EndpointReference"/> based on the <see cref="EndpointAnnotation"/> annotations from the resources' <see cref="IResource.Annotations"/> collection.</returns>
    [AspireExportIgnore(Reason = "Network-specific endpoint enumeration is not part of the ATS surface.")]
    public static IEnumerable<EndpointReference> GetEndpoints(this IResourceWithEndpoints resource, NetworkIdentifier contextNetworkId)
    {
        if (TryGetAnnotationsOfType<EndpointAnnotation>(resource, out var endpoints))
        {
            return endpoints.Select(e => new EndpointReference(resource, e, contextNetworkId));
        }
 
        return [];
    }
 
    /// <summary>
    /// Gets an endpoint reference for the specified endpoint name.
    /// </summary>
    /// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
    /// <param name="endpointName">The name of the endpoint.</param>
    /// <returns>An <see cref="EndpointReference"/>object providing resolvable reference for the specified endpoint.</returns>
    [AspireExportIgnore(Reason = "Resource handle endpoint lookup is not part of the ATS surface; use builder-based endpoint exports instead.")]
    public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName)
    {
        var endpoint = resource.TryGetEndpoints(out var endpoints) ?
            endpoints.FirstOrDefault(e => string.Equals(e.Name, endpointName, StringComparisons.EndpointAnnotationName)) :
            null;
        if (endpoint is null)
        {
            return new EndpointReference(resource, endpointName);
        }
        else
        {
            return new EndpointReference(resource, endpoint);
        }
    }
 
    /// <summary>
    /// Gets an endpoint reference for the specified endpoint name.
    /// </summary>
    /// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
    /// <param name="endpointName">The name of the endpoint.</param>
    /// <param name="contextNetworkId">The network ID of the network that provides the context for the returned <see cref="EndpointReference"/></param>
    /// <returns>An <see cref="EndpointReference"/>object providing resolvable reference for the specified endpoint.</returns>
    [AspireExportIgnore(Reason = "Network-specific endpoint lookup is not part of the ATS surface.")]
    public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName, NetworkIdentifier contextNetworkId)
    {
 
        var endpoint = resource.TryGetEndpoints(out var endpoints) ?
            endpoints.FirstOrDefault(e => string.Equals(e.Name, endpointName, StringComparisons.EndpointAnnotationName)) :
            null;
        if (endpoint is null)
        {
            return new EndpointReference(resource, endpointName, contextNetworkId);
        }
        else
        {
            return new EndpointReference(resource, endpoint, contextNetworkId);
        }
    }
 
    /// <summary>
    /// Resolves endpoint port configuration for the specified resource.
    /// Computes target ports and exposed ports based on resource type, endpoint configuration,
    /// and whether the endpoint is considered a default HTTP endpoint.
    /// </summary>
    /// <param name="resource">The resource containing endpoints to resolve.</param>
    /// <param name="portAllocator">Optional port allocator. If null, uses default allocation starting from port 8000.</param>
    /// <returns>A read-only list of resolved endpoints with computed port values.</returns>
    [AspireExportIgnore(Reason = "Endpoint resolution exposes infrastructure-specific types that are not part of the ATS surface.")]
    public static IReadOnlyList<ResolvedEndpoint> ResolveEndpoints(this IResource resource, IPortAllocator? portAllocator = null)
    {
        if (!resource.TryGetEndpoints(out var endpoints))
        {
            return [];
        }
 
        portAllocator ??= new PortAllocator();
        var httpSchemesEncountered = new HashSet<string>();
        var result = new List<ResolvedEndpoint>();
 
        foreach (var endpoint in endpoints)
        {
            var publicPort = EndpointAnnotation.NormalizePort(endpoint.Port);
            var configuredTargetPort = EndpointAnnotation.NormalizePort(endpoint.TargetPort);
 
            // Compute target port based on resource type and endpoint configuration
            ResolvedPort targetPort = (resource, endpoint.UriScheme, configuredTargetPort, publicPort) switch
            {
                // The port was explicitly specified so use it
                (_, _, int target, _) => ResolvedPort.Explicit(target),
 
                // Container resources get their default listening port from the exposed port (implicit)
                (ContainerResource, _, null, int port) => ResolvedPort.Implicit(port),
 
                // Check whether the project views this endpoint as Default (for its scheme).
                // If so, we don't specify the target port, as it will get one from the deployment tool.
                (ProjectResource, string uriScheme, null, _) when IsHttpScheme(uriScheme) && !httpSchemesEncountered.Contains(uriScheme) => ResolvedPort.None(),
 
                // Allocate a dynamic port
                _ => ResolvedPort.Allocated(portAllocator.AllocatePort())
            };
 
            // Track HTTP schemes encountered for ProjectResources
            if (resource is ProjectResource && IsHttpScheme(endpoint.UriScheme))
            {
                httpSchemesEncountered.Add(endpoint.UriScheme);
            }
 
            // Compute exposed port (host port)
            ResolvedPort exposedPort = (endpoint.UriScheme, publicPort, targetPort.Value) switch
            {
                // Port set explicitly, use it
                (_, int port, _) => ResolvedPort.Explicit(port),
 
                // We have a target port, infer the exposedPort from it
                (_, null, int targetPortValue) => ResolvedPort.Implicit(targetPortValue),
 
                // Let the tool infer the default http and https ports
                ("http", null, null) => ResolvedPort.None(),
                ("https", null, null) => ResolvedPort.None(),
 
                // Other schemes just allocate a port
                _ => ResolvedPort.Allocated(portAllocator.AllocatePort())
            };
 
            // Track used ports to avoid collisions when allocating
            if (exposedPort.Value is int ep)
            {
                portAllocator.AddUsedPort(ep);
            }
 
            if (targetPort.Value is int tp)
            {
                portAllocator.AddUsedPort(tp);
            }
 
            result.Add(new ResolvedEndpoint
            {
                Endpoint = endpoint,
                TargetPort = targetPort,
                ExposedPort = exposedPort
            });
        }
 
        return result;
 
        static bool IsHttpScheme(string scheme) => scheme is "http" or "https";
    }
 
    /// <summary>
    /// Attempts to get the container image name from the given resource.
    /// </summary>
    /// <param name="resource">The resource to get the container image name from.</param>
    /// <param name="imageName">The container image name if found, otherwise null.</param>
    /// <returns>True if the container image name was found, otherwise false.</returns>
    [AspireExportIgnore(Reason = "Container image inspection helper — not part of the ATS surface.")]
    public static bool TryGetContainerImageName(this IResource resource, [NotNullWhen(true)] out string? imageName)
    {
        return TryGetContainerImageName(resource, useBuiltImage: true, out imageName);
    }
 
    /// <summary>
    /// Attempts to get the container image name from the given resource.
    /// </summary>
    /// <param name="resource">The resource to get the container image name from.</param>
    /// <param name="useBuiltImage">When true, uses the image name from DockerfileBuildAnnotation if present. When false, uses only ContainerImageAnnotation.</param>
    /// <param name="imageName">The container image name if found, otherwise null.</param>
    /// <returns>True if the container image name was found, otherwise false.</returns>
    [AspireExportIgnore(Reason = "Container image inspection helper — not part of the ATS surface.")]
    public static bool TryGetContainerImageName(this IResource resource, bool useBuiltImage, [NotNullWhen(true)] out string? imageName)
    {
        // First check if there's a DockerfileBuildAnnotation with an image name/tag
        // This takes precedence over the ContainerImageAnnotation when building from a Dockerfile
        if (useBuiltImage &&
            resource.Annotations.OfType<DockerfileBuildAnnotation>().SingleOrDefault() is { } buildAnnotation &&
            !string.IsNullOrEmpty(buildAnnotation.ImageName))
        {
            var tagSuffix = string.IsNullOrEmpty(buildAnnotation.ImageTag) ? string.Empty : $":{buildAnnotation.ImageTag}";
            imageName = $"{buildAnnotation.ImageName}{tagSuffix}";
            return true;
        }
 
        if (resource.Annotations.OfType<ContainerImageAnnotation>().LastOrDefault() is { } imageAnnotation)
        {
            var registryPrefix = string.IsNullOrEmpty(imageAnnotation.Registry) ? string.Empty : $"{imageAnnotation.Registry}/";
 
            if (string.IsNullOrEmpty(imageAnnotation.SHA256))
            {
                var tagSuffix = string.IsNullOrEmpty(imageAnnotation.Tag) ? string.Empty : $":{imageAnnotation.Tag}";
                imageName = $"{registryPrefix}{imageAnnotation.Image}{tagSuffix}";
            }
            else
            {
                var shaSuffix = $"@sha256:{imageAnnotation.SHA256}";
                imageName = $"{registryPrefix}{imageAnnotation.Image}{shaSuffix}";
            }
 
            return true;
        }
 
        imageName = null;
        return false;
    }
 
    /// <summary>
    /// Gets the number of replicas for the specified resource. Defaults to <c>1</c> if no
    /// <see cref="ReplicaAnnotation" /> is found.
    /// </summary>
    /// <param name="resource">The resource to get the replica count for.</param>
    /// <returns>The number of replicas for the specified resource.</returns>
    [AspireExportIgnore(Reason = "Replica inspection helper — not part of the ATS surface.")]
    public static int GetReplicaCount(this IResource resource)
    {
        if (resource.TryGetLastAnnotation<ReplicaAnnotation>(out var replicaAnnotation))
        {
            return replicaAnnotation.Replicas;
        }
        else
        {
            return 1;
        }
    }
 
    /// <summary>
    /// Determines whether the specified resource requires image building.
    /// </summary>
    /// <remarks>
    /// Resources require an image build if they provide their own Dockerfile or are a project.
    /// Resources that are excluded from publishing are not considered to require image building.
    /// </remarks>
    /// <param name="resource">The resource to evaluate for image build requirements.</param>
    /// <returns>True if the resource requires image building; otherwise, false.</returns>
    [AspireExportIgnore(Reason = "Publishing inspection helper — not part of the ATS surface.")]
    public static bool RequiresImageBuild(this IResource resource)
    {
        if (resource.IsExcludedFromPublish())
        {
            return false;
        }
 
        return resource is ProjectResource || resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out _);
    }
 
    /// <summary>
    /// Determines whether the specified resource requires image building and pushing.
    /// </summary>
    /// <remarks>
    /// Resources require an image build and a push to a container registry if they provide
    /// their own Dockerfile or are a project.
    /// Resources that are excluded from publishing are not considered to require image building and pushing.
    /// </remarks>
    /// <param name="resource">The resource to evaluate for image push requirements.</param>
    /// <returns>True if the resource requires image building and pushing; otherwise, false.</returns>
    [AspireExportIgnore(Reason = "Publishing inspection helper — not part of the ATS surface.")]
    public static bool RequiresImageBuildAndPush(this IResource resource)
    {
        return resource.RequiresImageBuild() && !resource.IsBuildOnlyContainer();
    }
 
    internal static bool IsBuildOnlyContainer(this IResource resource)
    {
        return resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out var dockerfileBuild) &&
            !dockerfileBuild.HasEntrypoint;
    }
 
    /// <summary>
    /// Gets the compute environment that the resource is explicitly bound to, if any.
    /// </summary>
    /// <param name="resource">The resource to get the compute environment for.</param>
    /// <returns>The compute environment the resource is bound to, or <c>null</c> if the resource is not bound to any specific compute environment.</returns>
    [AspireExportIgnore(Reason = "Compute-environment inspection helper — not part of the ATS surface.")]
    public static IComputeEnvironmentResource? GetComputeEnvironment(this IResource resource)
    {
        if (resource.TryGetLastAnnotation<ComputeEnvironmentAnnotation>(out var computeEnvironmentAnnotation))
        {
            return computeEnvironmentAnnotation.ComputeEnvironment;
        }
        return null;
    }
 
    /// <summary>
    /// Gets the deployment target for the specified resource, if any. Throws an exception if
    /// there are multiple compute environments and a compute environment is not explicitly specified.
    /// </summary>
    [AspireExportIgnore(Reason = "Deployment target inspection helper — not part of the ATS surface.")]
    public static DeploymentTargetAnnotation? GetDeploymentTargetAnnotation(this IResource resource, IComputeEnvironmentResource? targetComputeEnvironment = null)
    {
        IComputeEnvironmentResource? selectedComputeEnvironment = null;
        if (resource.TryGetLastAnnotation<ComputeEnvironmentAnnotation>(out var computeEnvironmentAnnotation))
        {
            // If you have a ComputeEnvironmentAnnotation, it means the resource is bound to a specific compute environment.
            // Skip the annotation if it doesn't match the specified targetComputeEnvironment.
            if (targetComputeEnvironment is not null && targetComputeEnvironment != computeEnvironmentAnnotation.ComputeEnvironment)
            {
                return null;
            }
 
            // If the resource is bound to a specific compute environment, use that one.
            selectedComputeEnvironment = computeEnvironmentAnnotation.ComputeEnvironment;
        }
 
        if (resource.TryGetAnnotationsOfType<DeploymentTargetAnnotation>(out var deploymentTargetAnnotations))
        {
            var annotations = deploymentTargetAnnotations.ToArray();
 
            if (selectedComputeEnvironment is not null)
            {
                return annotations.SingleOrDefault(a => a.ComputeEnvironment == selectedComputeEnvironment);
            }
 
            if (annotations.Length > 1)
            {
                var computeEnvironmentNames = string.Join(", ", annotations.Select(a => a.ComputeEnvironment?.Name));
                throw new InvalidOperationException($"Resource '{resource.Name}' has multiple compute environments - '{computeEnvironmentNames}'. Please specify a single compute environment using 'WithComputeEnvironment'.");
            }
 
            var deploymentTargetAnnotation = annotations[0];
 
            // If you have a DeploymentTargetAnnotation, it means the resource is bound to a specific compute environment.
            // Skip the annotation if it doesn't match the specified targetComputeEnvironment.
            if (targetComputeEnvironment is not null && targetComputeEnvironment != deploymentTargetAnnotation.ComputeEnvironment)
            {
                return null;
            }
 
            return deploymentTargetAnnotation;
        }
        return null;
    }
 
    /// <summary>
    /// Gets the lifetime type for the specified resource.
    /// Defaults to <see cref="Lifetime.Session"/> if no lifetime annotation is found.
    /// </summary>
    /// <param name="resource">The resource to get the lifetime type for.</param>
    /// <returns>
    /// The <see cref="Lifetime"/> from the <see cref="PersistenceAnnotation"/> for the resource (if the annotation exists).
    /// Defaults to <see cref="Lifetime.Session"/> if the annotation is not set.
    /// </returns>
    internal static Lifetime GetLifetimeType(this IResource resource)
    {
        return GetLifetimeType(resource, []);
    }
 
    private static Lifetime GetLifetimeType(IResource resource, HashSet<IResource> visitedResources)
    {
        if (!visitedResources.Add(resource))
        {
            throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'.");
        }
 
        if (resource.TryGetLastAnnotation<PersistenceAnnotation>(out var persistenceAnnotation))
        {
            return persistenceAnnotation.Mode switch
            {
                PersistenceMode.Session => Lifetime.Session,
                PersistenceMode.Persistent => Lifetime.Persistent,
                PersistenceMode.Resource => persistenceAnnotation.SourceResource is { } sourceResource
                    ? GetLifetimeType(sourceResource, visitedResources)
                    : throw new InvalidOperationException($"Resource '{resource.Name}' has a resource persistence mode but no source resource."),
                PersistenceMode.ParentProcess => Lifetime.Persistent,
                _ => throw new InvalidOperationException($"Unknown persistence mode '{Enum.GetName(typeof(PersistenceMode), persistenceAnnotation.Mode)}'.")
            };
        }
 
        if (resource.TryGetLastAnnotation<ContainerLifetimeAnnotation>(out var containerLifetimeAnnotation))
        {
            return containerLifetimeAnnotation.Lifetime switch
            {
                ContainerLifetime.Session => Lifetime.Session,
                ContainerLifetime.Persistent => Lifetime.Persistent,
                _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), containerLifetimeAnnotation.Lifetime)}'.")
            };
        }
 
        return Lifetime.Session;
    }
 
    /// <summary>
    /// Determines whether the specified resource has a persistent lifetime.
    /// </summary>
    /// <param name="resource">The resource to get persistent lifetime behavior for.</param>
    /// <returns><see langword="true"/> if the resource has a persistent container or executable lifetime, otherwise <see langword="false"/>.</returns>
    internal static bool HasPersistentLifetime(this IResource resource)
    {
        return resource.GetLifetimeType() == Lifetime.Persistent;
    }
 
    internal static string GetOtelServiceInstanceId(this IResource resource, DcpInstance instance)
    {
        return resource.GetLifetimeType() == Lifetime.Persistent ? instance.Name : instance.Suffix;
    }
 
    /// <summary>
    /// Determines whether the specified resource has a parent process lifetime.
    /// </summary>
    /// <param name="resource">The resource to get parent process lifetime behavior for.</param>
    /// <param name="parentProcessId">The parent process ID if one exists.</param>
    /// <param name="parentProcessTimestamp">The parent process identity timestamp if one exists.</param>
    /// <returns><see langword="true"/> if the resource has a parent process lifetime, otherwise <see langword="false"/>.</returns>
    internal static bool TryGetParentProcessLifetime(this IResource resource, out int parentProcessId, out DateTime parentProcessTimestamp)
    {
        return TryGetParentProcessLifetime(resource, [], out parentProcessId, out parentProcessTimestamp);
    }
 
    private static bool TryGetParentProcessLifetime(IResource resource, HashSet<IResource> visitedResources, out int parentProcessId, out DateTime parentProcessTimestamp)
    {
        if (!visitedResources.Add(resource))
        {
            throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'.");
        }
 
        if (resource.TryGetLastAnnotation<PersistenceAnnotation>(out var persistenceAnnotation))
        {
            switch (persistenceAnnotation.Mode)
            {
                case PersistenceMode.ParentProcess when persistenceAnnotation.ParentProcessId is { } id && persistenceAnnotation.ParentProcessTimestamp is { } timestamp:
                    parentProcessId = id;
                    parentProcessTimestamp = timestamp;
                    return true;
                case PersistenceMode.ParentProcess:
                    throw new InvalidOperationException($"Resource '{resource.Name}' has a parent process persistence mode but no parent process identity.");
                case PersistenceMode.Resource:
                    return persistenceAnnotation.SourceResource is { } sourceResource
                        ? TryGetParentProcessLifetime(sourceResource, visitedResources, out parentProcessId, out parentProcessTimestamp)
                        : throw new InvalidOperationException($"Resource '{resource.Name}' has a resource persistence mode but no source resource.");
                case PersistenceMode.Session or PersistenceMode.Persistent:
                    parentProcessId = 0;
                    parentProcessTimestamp = default;
                    return false;
            }
        }
 
        parentProcessId = 0;
        parentProcessTimestamp = default;
        return false;
    }
 
    /// <summary>
    /// Determines whether the specified resource has a pull policy annotation and retrieves the value if it does.
    /// </summary>
    /// <param name="resource">The resource to check for a ContainerPullPolicy annotation</param>
    /// <param name="pullPolicy">The <see cref="ImagePullPolicy"/> for the annotation</param>
    /// <returns>True if an annotation exists, false otherwise</returns>
    internal static bool TryGetContainerImagePullPolicy(this IResource resource, [NotNullWhen(true)] out ImagePullPolicy? pullPolicy)
    {
        if (resource.TryGetLastAnnotation<ContainerImagePullPolicyAnnotation>(out var pullPolicyAnnotation))
        {
            pullPolicy = pullPolicyAnnotation.ImagePullPolicy;
            return true;
        }
 
        pullPolicy = null;
        return false;
    }
 
    /// <summary>
    /// Determines whether a resource has proxy support enabled or not. Resources may have a <see cref="ProxySupportAnnotation"/> setting that disables proxying for their
    /// endpoints regardless of the endpoint proxy configuration.
    /// </summary>
    /// <param name="resource">The resource to get proxy support for.</param>
    /// <returns>True if the resource supports proxied endpoints/services, false otherwise.</returns>
    internal static bool SupportsProxy(this IResource resource)
    {
        // If the resource doesn't have a ProxySupportAnnotation or the ProxyEnabled property on the annotation is true, then the resource supports proxying.
        return !resource.TryGetLastAnnotation<ProxySupportAnnotation>(out var proxySupportAnnotation) || proxySupportAnnotation.ProxyEnabled;
    }
 
    /// <summary>
    /// Get the top resource in the resource hierarchy.
    /// e.g. for a AzureBlobStorageResource, the top resource is the AzureStorageResource.
    /// </summary>
    internal static IResource GetRootResource(this IResource resource) =>
        resource switch
        {
            IResourceWithParent resWithParent => resWithParent.Parent.GetRootResource(),
            _ => resource
        };
 
    /// <summary>
    /// Returns a single DCP resource name for the specified resource.
    /// Throws <see cref="InvalidOperationException"/> if the resource has no resolved names or multiple resolved names.
    /// </summary>
    internal static string GetResolvedResourceName(this IResource resource)
    {
        var names = resource.GetResolvedResourceNames();
        if (names.Length == 0)
        {
            throw new InvalidOperationException($"Resource '{resource.Name}' has no resolved names.");
        }
        if (names.Length > 1)
        {
            throw new InvalidOperationException($"Resource '{resource.Name}' has multiple resolved names: {string.Join(", ", names)}.");
        }
 
        return names[0];
    }
 
    /// <summary>
    /// Returns the display name for the specified resource.
    /// For resources with replicas, returns the full <paramref name="resourceId"/> to identify the instance.
    /// For single-instance resources, returns the resource's display name without the DCP suffix.
    /// </summary>
    internal static string GetResolvedDisplayResourceName(this IResource resource, string resourceId)
    {
        return resource.GetReplicaCount() > 1 ? resourceId : resource.Name;
    }
 
    /// <summary>
    /// Attempts to get the DCP instances for the specified resource.
    /// </summary>
    /// <param name="resource">The resource to get the DCP instances from.</param>
    /// <param name="instances">When this method returns, contains the DCP instances if found and not empty; otherwise, an empty array.</param>
    /// <returns><see langword="true"/> if the resource has a non-empty DCP instances annotation; otherwise, <see langword="false"/>.</returns>
    internal static bool TryGetInstances(this IResource resource, out ImmutableArray<DcpInstance> instances)
    {
        if (resource.TryGetLastAnnotation<DcpInstancesAnnotation>(out var annotation) && !annotation.Instances.IsEmpty)
        {
            instances = annotation.Instances;
            return true;
        }
 
        instances = [];
        return false;
    }
 
    /// <summary>
    /// Gets resolved names for the specified resource.
    /// DCP resources are given a unique suffix as part of the complete name. We want to use that value.
    /// Also, a DCP resource could have multiple instances. All instance names are returned for a resource.
    /// </summary>
    internal static string[] GetResolvedResourceNames(this IResource resource)
    {
        if (resource.TryGetInstances(out var instances))
        {
            return instances.Select(i => i.Name).ToArray();
        }
        else
        {
            return [resource.Name];
        }
    }
 
    /// <summary>
    /// Processes image push options callbacks for the specified resource.
    /// </summary>
    /// <param name="resource">The resource to process image push options for.</param>
    /// <param name="cancellationToken">A cancellation token to observe while processing.</param>
    /// <returns>The resolved image push options.</returns>
    [Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")]
    internal static async Task<ContainerImagePushOptions> ProcessImagePushOptionsCallbackAsync(
        this IResource resource,
        CancellationToken cancellationToken)
    {
        var options = new ContainerImagePushOptions
        {
            RemoteImageName = resource.Name.ToLowerInvariant(),
            RemoteImageTag = "latest"
        };
 
        var context = new ContainerImagePushOptionsCallbackContext
        {
            Resource = resource,
            CancellationToken = cancellationToken,
            Options = options
        };
 
        var callbacks = resource.Annotations.OfType<ContainerImagePushOptionsCallbackAnnotation>();
 
        foreach (var callback in callbacks)
        {
            await callback.Callback(context).ConfigureAwait(false);
        }
 
        return options;
    }
 
    /// <summary>
    /// Gets the container registry associated with the specified resource.
    /// </summary>
    /// <param name="resource">The resource to get the container registry for.</param>
    /// <returns>The container registry associated with the resource.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the resource does not have a container registry reference.</exception>
    /// <remarks>
    /// This method checks for a container registry in the following order:
    /// <list type="number">
    /// <item>The <see cref="ContainerRegistryReferenceAnnotation"/> on the resource (set via <c>WithContainerRegistry</c>).</item>
    /// <item>The <see cref="DeploymentTargetAnnotation"/> on the resource.</item>
    /// <item>The <see cref="RegistryTargetAnnotation"/> on the resource (automatically added when a registry is added to the app model).</item>
    /// </list>
    /// </remarks>
    internal static IContainerRegistry GetContainerRegistry(this IResource resource)
    {
        // Try ContainerRegistryReferenceAnnotation (explicit WithContainerRegistry call)
        var registryAnnotation = resource.Annotations.OfType<ContainerRegistryReferenceAnnotation>().LastOrDefault();
        if (registryAnnotation is not null)
        {
            return registryAnnotation.Registry;
        }
 
        // Try to get the container registry from DeploymentTargetAnnotation first
        var deploymentTarget = resource.GetDeploymentTargetAnnotation();
        if (deploymentTarget?.ContainerRegistry is not null)
        {
            return deploymentTarget.ContainerRegistry;
        }
 
        // Fall back to RegistryTargetAnnotation (added automatically via BeforeStartEvent)
        var registryTargetAnnotations = resource.Annotations.OfType<RegistryTargetAnnotation>().ToArray();
        if (registryTargetAnnotations.Length == 1)
        {
            return registryTargetAnnotations[0].Registry;
        }
 
        if (registryTargetAnnotations.Length > 1)
        {
            var registryNames = string.Join(", ", registryTargetAnnotations.Select(a => a.Registry is IResource res ? res.Name : a.Registry.ToString()));
            throw new InvalidOperationException(
                $"Resource '{resource.Name}' has multiple container registries available - '{registryNames}'. " +
                $"Please specify which registry to use with '.WithContainerRegistry(registryBuilder)'.");
        }
 
        throw new InvalidOperationException($"Resource '{resource.Name}' does not have a container registry reference.");
    }
 
    /// <summary>
    /// Gets the full remote image name for the specified resource, including registry endpoint and tag.
    /// </summary>
    /// <param name="resource">The resource to get the remote image name for.</param>
    /// <param name="cancellationToken">A cancellation token to observe while processing.</param>
    /// <returns>The fully qualified remote image name.</returns>
    /// <exception cref="InvalidOperationException">Thrown when the resource does not have a container registry reference.</exception>
    /// <remarks>
    /// This method processes any image push options callbacks on the resource and combines the result
    /// with the container registry to produce the full remote image name.
    /// </remarks>
    [Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")]
    internal static async Task<string> GetFullRemoteImageNameAsync(
        this IResource resource,
        CancellationToken cancellationToken)
    {
        var pushOptions = await resource.ProcessImagePushOptionsCallbackAsync(cancellationToken).ConfigureAwait(false);
        var registry = resource.GetContainerRegistry();
        return await pushOptions.GetFullRemoteImageNameAsync(registry, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Gets the archive file path for a container image. This is the single calculation shared by the
    /// container runtimes that write the archive and by <see cref="ContainerImageReference"/>, which hands
    /// the path to consumers.
    /// </summary>
    /// <param name="outputPath">The output directory path.</param>
    /// <param name="imageName">The image name. May be registry-qualified and may include the tag.</param>
    /// <param name="imageTag">The image tag, when it is not already part of <paramref name="imageName"/>.</param>
    /// <returns>The full path to the archive file with .tar extension.</returns>
    /// <remarks>
    /// Producers and consumers must agree on this path, otherwise the archive is written to one location
    /// and looked up at another. Callers supply the image name in one of two shapes — combined
    /// (<c>myapp:latest</c>) or split (<c>myapp</c> + <c>latest</c>) — and both must resolve identically,
    /// which they do because <c>:</c> flattens to the same separator the split form joins with.
    /// </remarks>
    internal static string GetContainerImageArchivePath(string outputPath, string imageName, string? imageTag = null)
    {
        var fileName = string.IsNullOrEmpty(imageTag)
            ? $"{FlattenContainerImageName(imageName)}.tar"
            : $"{FlattenContainerImageName(imageName)}-{FlattenContainerImageName(imageTag)}.tar";
        return Path.Combine(outputPath, fileName);
    }
 
    /// <summary>
    /// Flattens a <c>&lt;registry&gt;/&lt;repository&gt;:&lt;tag&gt;</c> image name into a single
    /// file-name-safe segment, so that neither a repository segment nor the tag turns into a directory.
    /// </summary>
    internal static string FlattenContainerImageName(string imageName) => imageName.Replace('/', '-').Replace(':', '-');
 
    /// <summary>
    /// Gets a logger for the specified resource using the provided service provider.
    /// </summary>
    /// <param name="resource">The resource to get the logger for.</param>
    /// <param name="serviceProvider">The service provider to resolve dependencies.</param>
    /// <returns>A logger instance for the specified resource.</returns>
    internal static ILogger GetLogger(this IResource resource, IServiceProvider serviceProvider)
    {
        var resourceLoggerService = serviceProvider.GetRequiredService<ResourceLoggerService>();
        return resourceLoggerService.GetLogger(resource);
    }
 
    /// <summary>
    /// Computes the set of resources that the specified <paramref name="resource"/> depends on.
    /// </summary>
    /// <param name="resource">The resource to compute dependencies for.</param>
    /// <param name="executionContext">The execution context for resolving environment variables and arguments.</param>
    /// <param name="mode">Specifies dependency discovery mode.</param>
    /// <param name="cancellationToken">A cancellation token to observe while computing dependencies.</param>
    /// <returns>A set of all resources that the specified resource depends on.</returns>
    /// <remarks>
    /// <para>
    /// Dependencies are computed from multiple sources:
    /// <list type="bullet">
    /// <item>Parent resources via <see cref="IResourceWithParent"/></item>
    /// <item>Wait dependencies via <see cref="WaitAnnotation"/></item>
    /// <item>Connection string redirects via <see cref="ConnectionStringRedirectAnnotation"/></item>
    /// <item>References to endpoints in environment variables and command-line arguments (via <see cref="IValueWithReferences"/>)</item>
    /// </list>
    /// </para>
    /// <para>
    /// When <paramref name="mode"/> is <see cref="ResourceDependencyDiscoveryMode.DirectOnly"/>, only the immediate
    /// dependencies are returned. When <paramref name="mode"/> is <see cref="ResourceDependencyDiscoveryMode.Recursive"/>,
    /// all transitive dependencies are included.
    /// </para>
    /// <para>
    /// This method invokes environment variable and command-line argument callbacks to discover all references. The context resource (<paramref name="resource"/>) is not considered a dependency (even if it is transitively referenced).
    /// </para>
    /// </remarks>
    [AspireExportIgnore(Reason = "Dependency discovery helper depends on execution context and is not part of the ATS surface.")]
    public static Task<IReadOnlySet<IResource>> GetResourceDependenciesAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        ResourceDependencyDiscoveryMode mode = ResourceDependencyDiscoveryMode.Recursive,
        CancellationToken cancellationToken = default)
    {
        return GetDependenciesAsync([resource], executionContext, new ResourceDependencyDiscoveryOptions { DiscoveryMode = mode }, cancellationToken);
    }
 
    /// <summary>
    /// Computes the set of resources that the specified <paramref name="resource"/> depends on.
    /// </summary>
    /// <param name="resource">The resource to compute dependencies for.</param>
    /// <param name="executionContext">The execution context for resolving environment variables and arguments.</param>
    /// <param name="options">Changes details of dependency discovery process. See <see cref="ResourceDependencyDiscoveryOptions"/> enumeration for more information.</param>
    /// <param name="cancellationToken">A cancellation token to observe while computing dependencies.</param>
    /// <returns>A set of all resources that the specified resource depends on.</returns>
    /// <remarks>
    /// <para>
    /// Dependencies are computed from multiple sources:
    /// <list type="bullet">
    /// <item>Parent resources via <see cref="IResourceWithParent"/></item>
    /// <item>Wait dependencies via <see cref="WaitAnnotation"/></item>
    /// <item>Connection string redirects via <see cref="ConnectionStringRedirectAnnotation"/></item>
    /// <item>References to endpoints in environment variables and command-line arguments (via <see cref="IValueWithReferences"/>)</item>
    /// </list>
    /// </para>
    /// <para>
    /// This method invokes environment variable and command-line argument callbacks to discover all references. The context resource (<paramref name="resource"/>) is not considered a dependency (even if it is transitively referenced).
    /// </para>
    /// </remarks>
    [AspireExportIgnore(Reason = "Parameters and return type are not ATS-compatible — internal dependency discovery helper.")]
    public static Task<IReadOnlySet<IResource>> GetResourceDependenciesAsync(
        this IResource resource,
        DistributedApplicationExecutionContext executionContext,
        ResourceDependencyDiscoveryOptions options,
        CancellationToken cancellationToken = default)
    {
        return GetDependenciesAsync([resource], executionContext, options, cancellationToken);
    }
 
    /// <summary>
    /// Efficiently computes the set of resources that the specified source set of resources depends on.
    /// </summary>
    /// <param name="resources">The source set of resources to compute dependencies for.</param>
    /// <param name="executionContext">The execution context for resolving environment variables and arguments.</param>
    /// <param name="options">Changes details of dependency discovery process. See <see cref="ResourceDependencyDiscoveryOptions"/> enumeration for more information.</param>
    /// <param name="cancellationToken">A cancellation token to observe while computing dependencies.</param>
    /// <returns>A set of all resources that the specified resource depends on.</returns>
    /// <remarks>
    /// <para>
    /// Dependencies are computed from multiple sources:
    /// <list type="bullet">
    /// <item>Parent resources via <see cref="IResourceWithParent"/></item>
    /// <item>Wait dependencies via <see cref="WaitAnnotation"/></item>
    /// <item>Connection string redirects via <see cref="ConnectionStringRedirectAnnotation"/></item>
    /// <item>References to endpoints in environment variables and command-line arguments (via <see cref="IValueWithReferences"/>)</item>
    /// </list>
    /// </para>
    /// <para>
    /// This method invokes environment variable and command-line argument callbacks to discover all references.
    /// </para>
    /// </remarks>
    internal static async Task<IReadOnlySet<IResource>> GetDependenciesAsync(
        IEnumerable<IResource> resources,
        DistributedApplicationExecutionContext executionContext,
        ResourceDependencyDiscoveryOptions? options = default,
        CancellationToken cancellationToken = default)
    {
        var dependencies = new HashSet<IResource>();
        var newDependencies = new HashSet<IResource>();
        var toProcess = new Queue<IResource>();
        options ??= new ResourceDependencyDiscoveryOptions { DiscoveryMode = ResourceDependencyDiscoveryMode.Recursive };
 
        foreach (var resource in resources)
        {
            newDependencies.Clear();
            await GatherDirectDependenciesAsync(resource, dependencies, newDependencies, executionContext, options, cancellationToken).ConfigureAwait(false);
 
            if (options.DiscoveryMode == ResourceDependencyDiscoveryMode.Recursive)
            {
                // Compute transitive closure by recursively processing dependencies
 
                foreach (var nd in newDependencies)
                {
                    toProcess.Enqueue(nd);
                }
 
                while (toProcess.Count > 0)
                {
                    var dep = toProcess.Dequeue();
                    newDependencies.Clear();
 
                    await GatherDirectDependenciesAsync(dep, dependencies, newDependencies, executionContext, options, cancellationToken).ConfigureAwait(false);
 
                    foreach (var newDep in newDependencies)
                    {
                        if (newDep != resource)
                        {
                            toProcess.Enqueue(newDep);
                        }
                    }
                }
            }
        }
 
        // Ensure the input resources are not in its own dependency set, even if referenced transitively.
        foreach (var resource in resources)
        {
            dependencies.Remove(resource);
        }
 
        return dependencies;
    }
 
    /// <summary>
    /// Gathers direct dependencies of a given resource.
    /// </summary>
    /// <param name="resource">The resource to gather dependencies for.</param>
    /// <param name="dependencies">The set of dependencies (where dependency resources will be placed).</param>
    /// <param name="newDependencies">The set of newly discovered dependencies in this invocation (not present in <paramref name="dependencies"/> at the moment of invocation).</param>
    /// <param name="executionContext">The execution context for resolving environment variables and arguments.</param>
    /// <param name="options">Changes details of dependency discovery process.</param>
    /// <param name="cancellationToken">A cancellation token to observe while gathering dependencies.</param>
    private static async Task GatherDirectDependenciesAsync(
        IResource resource,
        HashSet<IResource> dependencies,
        HashSet<IResource> newDependencies,
        DistributedApplicationExecutionContext executionContext,
        ResourceDependencyDiscoveryOptions options,
        CancellationToken cancellationToken)
    {
        var visited = new HashSet<object>();
 
        // Collect direct dependencies from annotations
        CollectAnnotationDependencies(resource, dependencies, newDependencies);
 
        // Collect raw (unresolved) environment variable and argument values
        var rawValues = await GatherRawEnvironmentAndArgumentValuesAsync(resource, executionContext, options, cancellationToken).ConfigureAwait(false);
 
        foreach (var value in rawValues)
        {
            CollectDependenciesFromValue(value, dependencies, newDependencies, visited, executionContext);
        }
    }
 
    /// <summary>
    /// Gathers raw (unresolved) environment variable and argument values from a resource.
    /// </summary>
    private static async Task<List<object>> GatherRawEnvironmentAndArgumentValuesAsync(
        IResource resource,
        DistributedApplicationExecutionContext executionContext,
        ResourceDependencyDiscoveryOptions options,
        CancellationToken cancellationToken)
    {
        var rawValues = new List<object>();
 
        // Gather environment variable values
        if (resource.TryGetEnvironmentVariables(out var envAnnotations))
        {
            if (options.PeekCachedCallbackResultsOnly)
            {
                // Read-only discovery: never invoke a callback. Only harvest values DCP has already resolved
                // and cached via EvaluateOnceAsync. Skip in-flight/faulted/canceled tasks so we never block
                // describe on an unresolved value nor observe a poisoned task.
                foreach (var ann in envAnnotations)
                {
                    if (ann.AsCallbackAnnotation().TryGetCachedResult(out var cachedTask) &&
                        cachedTask!.IsCompletedSuccessfully)
                    {
                        rawValues.AddRange(cachedTask.Result.Values);
                    }
                }
            }
            else
            {
                var envVars = new Dictionary<string, object>();
                var context = new EnvironmentCallbackContext(executionContext, resource, envVars, cancellationToken: cancellationToken);
 
                if (options.CacheAnnotationCallbackResults)
                {
                    foreach (var ann in envAnnotations)
                    {
                        var resultingVars = await ann.AsCallbackAnnotation().EvaluateOnceAsync(context).ConfigureAwait(false);
                        rawValues.AddRange(resultingVars.Values);
                    }
 
                }
                else
                {
                    foreach (var ann in envAnnotations)
                    {
                        await ann.Callback(context).ConfigureAwait(false);
                    }
                    rawValues.AddRange(envVars.Values);
                }
            }
        }
 
        // Gather command-line argument values
        if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var argAnnotations))
        {
            if (options.PeekCachedCallbackResultsOnly)
            {
                foreach (var ann in argAnnotations)
                {
                    if (ann.AsCallbackAnnotation().TryGetCachedResult(out var cachedTask) &&
                        cachedTask!.IsCompletedSuccessfully)
                    {
                        rawValues.AddRange(cachedTask.Result);
                    }
                }
            }
            else
            {
                var args = new List<object>();
                var context = new CommandLineArgsCallbackContext(args, resource, cancellationToken)
                {
                    ExecutionContext = executionContext
                };
 
                if (options.CacheAnnotationCallbackResults)
                {
                    foreach (var ann in argAnnotations)
                    {
                        var resultingArgs = await ann.AsCallbackAnnotation().EvaluateOnceAsync(context).ConfigureAwait(false);
                        rawValues.AddRange(resultingArgs);
                    }
                }
                else
                {
                    foreach (var ann in argAnnotations)
                    {
                        await ann.Callback(context).ConfigureAwait(false);
                    }
                    rawValues.AddRange(args);
                }
            }
        }
 
        var launchToolArgs = await GatherLaunchToolArgumentValuesAsync(
            resource,
            executionContext,
            NullLogger.Instance,
            options.CacheAnnotationCallbackResults,
            options.PeekCachedCallbackResultsOnly,
            cancellationToken).ConfigureAwait(false);
        rawValues.AddRange(launchToolArgs);
 
        return rawValues;
    }
 
    /// <summary>
    /// Collects dependencies from resource annotations (parent, wait, connection string redirect).
    /// </summary>
    /// <returns>A set of newly collected dependencies added to <paramref name="dependencies"/>.</returns>
    private static void CollectAnnotationDependencies(IResource resource, HashSet<IResource> dependencies, HashSet<IResource> newDependencies)
    {
        // Parent relationship
        if (resource is IResourceWithParent resourceWithParent)
        {
            if (dependencies.Add(resourceWithParent.Parent))
            {
                newDependencies.Add(resourceWithParent.Parent);
            }
        }
 
        // Wait annotations
        if (resource.TryGetAnnotationsOfType<WaitAnnotation>(out var waitAnnotations))
        {
            foreach (var waitAnnotation in waitAnnotations)
            {
                if (dependencies.Add(waitAnnotation.Resource))
                {
                    newDependencies.Add(waitAnnotation.Resource);
                }
            }
        }
 
        // Connection string redirect
        if (resource.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var redirectAnnotation))
        {
            if (dependencies.Add(redirectAnnotation.Resource))
            {
                newDependencies.Add(redirectAnnotation.Resource);
            }
        }
    }
 
    /// <summary>
    /// Recursively collects resource dependencies from a value using <see cref="IValueWithReferences"/>.
    /// </summary>
    private static void CollectDependenciesFromValue(
        object? value,
        HashSet<IResource> dependencies,
        HashSet<IResource> newDependencies,
        HashSet<object> visitedValues,
        DistributedApplicationExecutionContext executionContext)
    {
        if (value is null || !visitedValues.Add(value))
        {
            return;
        }
 
        if (value is HostUrl hostUrl)
        {
            CollectHostUrlDependencies(hostUrl, dependencies, newDependencies, executionContext);
        }
 
        // Direct resource references
        if (value is IResource resource)
        {
            if (dependencies.Add(resource))
            {
                newDependencies.Add(resource);
            }
        }
 
        // Resource builder wrapping a resource
        if (value is IResourceBuilder<IResource> resourceBuilder)
        {
            if (dependencies.Add(resourceBuilder.Resource))
            {
                newDependencies.Add(resourceBuilder.Resource);
            }
            value = resourceBuilder.Resource;
        }
 
        // Recurse through IValueWithReferences
        if (value is IValueWithReferences valueWithReferences)
        {
            foreach (var reference in valueWithReferences.References)
            {
                CollectDependenciesFromValue(reference, dependencies, newDependencies, visitedValues, executionContext);
            }
        }
    }
 
    private static void CollectHostUrlDependencies(
        HostUrl hostUrl,
        HashSet<IResource> dependencies,
        HashSet<IResource> newDependencies,
        DistributedApplicationExecutionContext executionContext)
    {
        if (!HostUrl.TryGetLocalHostPort(hostUrl.Url, out var port))
        {
            return;
        }
 
        DistributedApplicationModel? model;
        try
        {
            model = executionContext.Services.GetService<DistributedApplicationModel>();
        }
        catch (InvalidOperationException)
        {
            return;
        }
 
        if (model is null)
        {
            return;
        }
 
        foreach (var resource in model.Resources.Where(r => !r.IsContainer()).OfType<IResourceWithEndpoints>())
        {
            if (resource.Annotations.OfType<EndpointAnnotation>().Any(ep => HostUrl.MatchesHostPort(ep, port)) && dependencies.Add(resource))
            {
                newDependencies.Add(resource);
            }
        }
    }
 
#pragma warning disable ASPIREDOTNETTOOL // DotnetToolResource is experimental
    /// <summary>
    /// Gets the resource type string for the specified resource.
    /// </summary>
    internal static string GetResourceType(this IResource resource) => resource switch
    {
        ProjectResource => KnownResourceTypes.Project,
        ContainerResource => KnownResourceTypes.Container,
        ContainerExecutableResource => KnownResourceTypes.ContainerExec,
        DotnetToolResource => KnownResourceTypes.Tool,
        ExecutableResource when resource.HasAnnotationOfType<IProjectMetadata>() => KnownResourceTypes.Project,
        ExecutableResource => KnownResourceTypes.Executable,
        ParameterResource => KnownResourceTypes.Parameter,
        ConnectionStringResource => KnownResourceTypes.ConnectionString,
        ExternalServiceResource => KnownResourceTypes.ExternalService,
        _ => resource.GetType().Name
    };
#pragma warning restore ASPIREDOTNETTOOL
}