File: Ats\CoreExports.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 Aspire.Hosting.ApplicationModel;
 
namespace Aspire.Hosting.Ats;
 
/// <summary>
/// Core ATS (Aspire Type System) exports for polyglot app host support.
/// </summary>
/// <remarks>
/// <para>
/// This class defines the foundational capabilities that enable non-.NET languages (TypeScript, Python, etc.)
/// to build Aspire distributed applications. These exports form the stable API surface for polyglot app hosts.
/// </para>
/// <para>
/// <strong>Design Principles:</strong>
/// <list type="bullet">
///   <item><description>Capabilities are the contract - not CLR method signatures</description></item>
///   <item><description>Handles replace direct object references - guest code never sees .NET types</description></item>
///   <item><description>Capability IDs use format {Package}/{Method}</description></item>
///   <item><description>.NET implementation details are hidden behind a stable polyglot surface</description></item>
/// </list>
/// </para>
/// <para>
/// <strong>Capability Naming Convention:</strong> <c>{Package}/{operation}</c>
/// </para>
/// <para>
/// <strong>Usage from TypeScript:</strong>
/// <code>
/// // Create builder and add resources
/// const builder = await client.invoke("Aspire.Hosting/createBuilder", {});
/// const redis = await client.invoke("Aspire.Hosting/addContainer", { builder, name: "cache", image: "redis:latest" });
/// await client.invoke("Aspire.Hosting/withEnvironment", { resource: redis, name: "REDIS_MODE", value: "standalone" });
///
/// // Build and run
/// const app = await client.invoke("Aspire.Hosting/build", { builder });
/// await client.invoke("Aspire.Hosting/run", { app });
/// </code>
/// </para>
/// </remarks>
internal static class CoreExports
{
    #region Application Lifecycle
 
    // Note: createBuilder is now on DistributedApplication.CreateBuilder
    // Note: build is now on IDistributedApplicationBuilder.Build via [AspireExport("build")]
    // Note: run is now on DistributedApplication.RunAsync via [AspireExport("run")]
    // Note: ExecutionContext, Configuration, Environment, and AppHostDirectory are accessed via property getters
    // on IDistributedApplicationBuilder which has [AspireExport(ExposeProperties = true)].
 
    // Note: getEndpoint is now on ResourceBuilderExtensions.GetEndpoint
    // Note: withReference is now on ResourceBuilderExtensions.WithReference
 
    #endregion
 
    #region Compute Configuration
 
    /// <summary>
    /// Adds a volume to a container resource.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Volumes persist data across container restarts. Named volumes are managed
    /// by Docker/Podman and stored in a system-managed location.
    /// </para>
    /// <para>
    /// <strong>Why this wrapper exists:</strong> The original <c>ContainerResourceBuilderExtensions.WithVolume</c>
    /// has parameter order <c>(name?, target, isReadOnly)</c> where the optional <c>name</c> comes first.
    /// This wrapper reorders parameters to <c>(target, name?, isReadOnly)</c> so the required <c>target</c>
    /// parameter comes first, providing a better API for polyglot consumers.
    /// </para>
    /// </remarks>
    /// <param name="resource">The container resource builder handle.</param>
    /// <param name="target">The mount path inside the container.</param>
    /// <param name="name">The volume name. If null, an anonymous volume is created.</param>
    /// <param name="isReadOnly">Whether the volume is read-only.</param>
    /// <returns>The same resource builder handle for chaining.</returns>
    /// <remarks>
    /// <para>
    /// This capability deliberately does not expose the C# <c>env</c> parameter. A container always
    /// receives <paramref name="target"/> as its effective volume path in every mode, so the C#
    /// convenience overload is exactly equivalent to <c>withVolume(target, name).withEnvironment(env, target)</c>
    /// in a polyglot AppHost. Keeping the exported parameter list frozen matters because the Rust
    /// generator emits optional capability parameters positionally and has no overloading, so appending
    /// a parameter here would be a source-breaking change for existing Rust AppHosts. Projects and
    /// executables genuinely need the parameter because their run-mode path is computed by the host,
    /// and they get it through the separate withProjectVolume/withExecutableVolume capabilities.
    /// </para>
    /// </remarks>
    [AspireExport]
    public static IResourceBuilder<ContainerResource> WithVolume(
        this IResourceBuilder<ContainerResource> resource,
        string target,
        string? name = null,
        bool isReadOnly = false)
    {
        return VolumeResourceBuilderExtensions.WithVolumeCore(resource, name, target, isReadOnly, env: null);
    }
 
    /// <summary>
    /// Adds a volume to a project resource.
    /// </summary>
    /// <param name="resource">The project resource builder handle.</param>
    /// <param name="target">The mount path inside the published container.</param>
    /// <param name="name">The volume name.</param>
    /// <param name="env">The environment variable that receives the effective volume path.</param>
    /// <param name="isReadOnly">Whether the published volume is read-only.</param>
    /// <returns>The same project resource builder handle for chaining.</returns>
    [AspireExport("withProjectVolume", MethodName = "withVolume")]
    public static IResourceBuilder<ProjectResource> WithProjectVolumeForPolyglot(
        this IResourceBuilder<ProjectResource> resource,
        string target,
        string name,
        string env,
        bool isReadOnly = false)
    {
        return WithProcessVolume(resource, target, name, isReadOnly, env);
    }
 
    /// <summary>
    /// Adds a volume to an executable resource.
    /// </summary>
    /// <param name="resource">The executable resource builder handle.</param>
    /// <param name="target">The mount path inside the published container.</param>
    /// <param name="name">The volume name.</param>
    /// <param name="env">The environment variable that receives the effective volume path.</param>
    /// <param name="isReadOnly">Whether the published volume is read-only.</param>
    /// <returns>The same executable resource builder handle for chaining.</returns>
    [AspireExport("withExecutableVolume", MethodName = "withVolume")]
    public static IResourceBuilder<ExecutableResource> WithExecutableVolumeForPolyglot(
        this IResourceBuilder<ExecutableResource> resource,
        string target,
        string name,
        string env,
        bool isReadOnly = false)
    {
        return WithProcessVolume(resource, target, name, isReadOnly, env);
    }
 
    private static IResourceBuilder<T> WithProcessVolume<T>(
        IResourceBuilder<T> resource,
        string target,
        string name,
        bool isReadOnly,
        string env)
        where T : IComputeResource, IResourceWithEnvironment
    {
        // These exports are the polyglot projection of the public WithVolume<T>(name, target, env, isReadOnly)
        // overload, so they have to reject the same inputs. WithVolumeCore only null-checks target because it is
        // shared with the container overloads, which have accepted an empty target since they shipped.
        ArgumentNullException.ThrowIfNull(resource);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentException.ThrowIfNullOrEmpty(target);
        ArgumentException.ThrowIfNullOrEmpty(env);
 
        return VolumeResourceBuilderExtensions.WithVolumeCore(resource, name, target, isReadOnly, env);
    }
 
    #endregion
 
    #region Resource Information
 
    /// <summary>
    /// Gets the name of the resource from a builder.
    /// </summary>
    /// <remarks>
    /// <strong>Why this wrapper exists:</strong> This capability accesses a nested property
    /// (<c>resource.Resource.Name</c>) which requires a wrapper method. There is no single
    /// .NET method that returns just the resource name that could be annotated directly.
    /// </remarks>
    /// <param name="resource">The resource builder handle.</param>
    /// <returns>The resource name.</returns>
    [AspireExport]
    public static string GetResourceName(this IResourceBuilder<IResource> resource)
    {
        return resource.Resource.Name;
    }
 
    #endregion
 
    #region Project Configuration
 
    /// <summary>
    /// Includes only the specified project endpoint names in environment-variable injection.
    /// </summary>
    /// <param name="resource">The project resource builder handle.</param>
    /// <param name="endpointNames">The endpoint names to include in environment variables.</param>
    /// <returns>The same project resource builder handle for chaining.</returns>
    [AspireExport]
    public static IResourceBuilder<ProjectResource> WithEndpointsInEnvironment(
        this IResourceBuilder<ProjectResource> resource,
        string[] endpointNames)
    {
        ArgumentNullException.ThrowIfNull(resource);
        ArgumentNullException.ThrowIfNull(endpointNames);
 
        var includedEndpointNames = endpointNames.ToHashSet(StringComparers.EndpointAnnotationName);
 
        return global::Aspire.Hosting.ProjectResourceBuilderExtensions.WithEndpointsInEnvironment(
            resource,
            endpoint => includedEndpointNames.Contains(endpoint.Name));
    }
 
    #endregion
 
    #region Parameters
 
    // Note: withDescription is now on ParameterResourceBuilderExtensions.WithDescription
 
    #endregion
}