File: Authoring\DistributedApplicationBuilder.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Client\Aspire.Hosting.Native.Client.csproj (Aspire.Hosting.Native.Client)
// 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.Native.Client;
using Microsoft.Extensions.Configuration;
 
namespace Aspire.Hosting.Native.Authoring;
 
/// <summary>Creates a synchronous authoring plan; creation never connects or performs RPC.</summary>
public static class DistributedApplication
{
    /// <summary>Creates a builder from framework/runtime configuration and ordinary application arguments.</summary>
    /// <param name="args">Framework selection and application configuration flags.</param>
    /// <returns>A synchronous, disconnected command recorder.</returns>
    public static DistributedApplicationBuilder CreateBuilder(string[] args) => CreateBuilder(args, new NativeBuilderOptions());
    /// <summary>Creates a command recorder using stable application identity and injected framework services.</summary>
    /// <param name="args">Framework selection and application configuration flags.</param>
    /// <param name="options">Application root/name, framework time and classified logging services.</param>
    /// <returns>A synchronous, disconnected command recorder.</returns>
    public static DistributedApplicationBuilder CreateBuilder(string[] args, NativeBuilderOptions options) =>
        new(NativeBuilderSettings.FromArgs(args, options));
}
 
/// <summary>Records typed composition commands. This is not a second runtime graph or credential evaluator.</summary>
public sealed partial class DistributedApplicationBuilder
{
    private readonly List<Func<NativeBuildContext, Task>> _declarations = [];
    private readonly List<Func<NativeBuildContext, Task>> _configuration = [];
    private readonly List<Resource> _resources = [];
    private readonly Dictionary<string, string> _providers = new(StringComparer.Ordinal);
    private bool _built;
    internal NativeBuilderSettings Settings { get; }
    /// <summary>Gets environment, supplied configuration and application arguments in increasing precedence.</summary>
    public IConfiguration Configuration => Settings.Configuration;
    /// <summary>Gets inert authored declarations for integration callback lookup, not native runtime state.</summary>
    public IReadOnlyList<Resource> Resources => _resources.AsReadOnly();
    internal IReadOnlyDictionary<string, string> Providers => _providers;
    internal DistributedApplicationBuilder(NativeBuilderSettings settings) => Settings = settings;
 
    /// <summary>Creates the same command recorder for a provider's delegated native composition.</summary>
    /// <returns>A disconnected integration-author builder.</returns>
    public static DistributedApplicationBuilder ForIntegration() => new(new());
 
    /// <summary>Adds an inert resource through the generic container primitive.</summary>
    /// <typeparam name="T">An integration-owned container resource type.</typeparam>
    /// <param name="resource">Inert authoring data.</param>
    /// <returns>The typed fluent container builder.</returns>
    public IResourceBuilder<T> AddContainer<T>(T resource) where T : ContainerResource
    {
        Add(resource);
        _declarations.Add(async context => context.Bind(resource,
            await context.Composition.AddContainerAsync(resource.Name, resource.Image, context.Token).ConfigureAwait(false)));
 
        return new ResourceBuilder<T>(this, resource);
    }
 
    /// <summary>Declares a generic container synchronously.</summary>
    /// <param name="name">The logical resource name.</param>
    /// <param name="image">The container image.</param>
    /// <returns>The typed fluent container builder.</returns>
    public IResourceBuilder<ContainerResource> AddContainer(string name, string image) => AddContainer(new ContainerResource(name, image));
 
    /// <summary>Declares an executable synchronously without starting a process.</summary>
    /// <param name="name">The logical resource name.</param>
    /// <param name="executablePath">The absolute executable path.</param>
    /// <param name="workingDirectory">The absolute working directory.</param>
    /// <returns>The typed fluent executable builder.</returns>
    public IResourceBuilder<ExecutableResource> AddExecutable(string name, string executablePath, string workingDirectory)
    {
        var resource = new ExecutableResource(name, executablePath, workingDirectory);
        Add(resource);
        _declarations.Add(async context => context.Bind(resource,
            await context.Composition.AddExecutableAsync(name, executablePath, workingDirectory, context.Token).ConfigureAwait(false)));
 
        return new ResourceBuilder<ExecutableResource>(this, resource);
    }
 
    /// <summary>Declares a parameter; absent secret values request a native-generated default credential.</summary>
    /// <param name="name">The parameter name.</param>
    /// <param name="value">An explicit value, or null to request native generation for a secret.</param>
    /// <param name="secret">Whether to store the value in native secret storage.</param>
    /// <returns>A symbolic parameter builder.</returns>
    public IResourceBuilder<ParameterResource> AddParameter(string name, string? value = null, bool secret = true)
    {
        var resource = new ParameterResource(name, secret);
        Add(resource);
        _declarations.Insert(0, async context => context.BindParameter(resource, secret
            ? value is null ? await context.Composition.AddGeneratedSecretParameterAsync(name, context.Token).ConfigureAwait(false) :
                await context.Composition.AddSecretParameterAsync(name, value, context.Token).ConfigureAwait(false)
            : await context.Composition.CreateLiteralValueAsync(value ?? throw new ArgumentException("A nonsecret parameter requires a value."),
                context.Token).ConfigureAwait(false)));
 
        return new ResourceBuilder<ParameterResource>(this, resource);
    }
 
    /// <summary>Records a provider-backed resource using generated projection calls, never raw capability IDs.</summary>
    /// <typeparam name="T">The inert integration resource type.</typeparam>
    /// <param name="resource">Integration-owned authoring data.</param>
    /// <param name="packageName">The selected provider package identity.</param>
    /// <param name="declare">The generated projection adapter, invoked only during asynchronous materialization.</param>
    /// <returns>The typed integration resource builder.</returns>
    public IResourceBuilder<T> AddIntegrationResource<T>(T resource, string packageName,
        Func<NativeBuildContext, Task<(DeclaredResource Declaration, ReferenceExpression Connection)>> declare) where T : Resource
    {
        Add(resource);
        _providers.TryAdd(packageName, packageName);
        _declarations.Add(async context =>
        {
            var declared = await declare(context).ConfigureAwait(false);
            context.Bind(resource, declared.Declaration);
            context.BindConnection(resource, declared.Connection);
        });
 
        return new ResourceBuilder<T>(this, resource);
    }
 
    /// <summary>Imports an already delegated parameter at the generated provider boundary, without reading its value.</summary>
    /// <param name="name">The authoring parameter name.</param>
    /// <param name="value">Original delegated native symbolic authority.</param>
    /// <returns>An inert parameter builder.</returns>
    public IResourceBuilder<ParameterResource> ImportParameter(string name, DeclaredValue value)
    {
        var resource = new ParameterResource(name, true);
        Add(resource);
        _declarations.Insert(0, context =>
        {
            context.BindParameter(resource, value);
            return Task.CompletedTask;
        });
 
        return new ResourceBuilder<ParameterResource>(this, resource);
    }
 
    private void Add(Resource resource)
    {
        EnsureMutable();
        ArgumentException.ThrowIfNullOrWhiteSpace(resource.Name);
        if (_resources.Any(existing => string.Equals(existing.Name, resource.Name, StringComparison.OrdinalIgnoreCase)))
        {
            throw new ArgumentException("Duplicate authored resource name.");
        }
        _resources.Add(resource);
    }
    internal void Record(Func<NativeBuildContext, Task> command)
    {
        EnsureMutable();
        _configuration.Add(command);
    }
    internal void EnsureMutable()
    {
        if (_built)
        {
            throw new InvalidOperationException("The authoring plan is sealed.");
        }
    }
    internal string VolumeName(Resource resource) => $"{Settings.VolumePrefix}-{resource.Name}-data";
    /// <summary>Gets the stable default name of a retained data volume, without creating it.</summary>
    /// <param name="resource">The authored volume owner.</param>
    /// <returns>The configured prefix, resource name and data suffix.</returns>
    public string GetDefaultDataVolumeName(Resource resource) => VolumeName(resource);
    /// <summary>Seals authoring commands without blocking on a network operation.</summary>
    /// <returns>A disconnected application runtime adapter.</returns>
    public NativeApplication Build()
    {
        EnsureMutable();
        _built = true;
 
        return new(this);
    }
    /// <summary>Materializes provider-side composition asynchronously through the same generated native projection.</summary>
    /// <param name="composition">The original delegated native composition.</param>
    /// <param name="cancellationToken">Cancels caller waits without replaying accepted native side effects.</param>
    /// <returns>The generated-handle adapter for this materialization.</returns>
    public async Task<NativeBuildContext> MaterializeAsync(Composition composition, CancellationToken cancellationToken)
    {
        _built = true;
        var context = new NativeBuildContext(composition, cancellationToken);
        foreach (var declaration in _declarations)
        {
            await declaration(context).ConfigureAwait(false);
        }
        foreach (var command in _configuration)
        {
            await command(context).ConfigureAwait(false);
        }
 
        return context;
    }
}
 
internal sealed class ResourceBuilder<T>(DistributedApplicationBuilder builder, T resource) : IResourceBuilder<T> where T : Resource
{
    public T Resource { get; } = resource;
    public DistributedApplicationBuilder ApplicationBuilder { get; } = builder;
}
 
/// <summary>Adapts typed command records to generated native handles at asynchronous transport boundaries.</summary>
/// <param name="composition">Original native composition authority.</param>
/// <param name="token">Materialization cancellation.</param>
public sealed class NativeBuildContext(Composition composition, CancellationToken token)
{
    private readonly Dictionary<Resource, DeclaredResource> _declarations = new(ReferenceEqualityComparer.Instance);
    private readonly Dictionary<ParameterResource, DeclaredValue> _parameters = new(ReferenceEqualityComparer.Instance);
    private readonly Dictionary<Resource, ReferenceExpression> _connections = new(ReferenceEqualityComparer.Instance);
    private readonly Dictionary<ReferenceExpression, Task<DeclaredValue>> _values = new(ReferenceEqualityComparer.Instance);
    /// <summary>Gets the generated composition projection for framework/provider adapters.</summary>
    public Composition Composition { get; } = composition;
    /// <summary>Gets materialization cancellation.</summary>
    public CancellationToken Token { get; } = token;
    /// <summary>Gets a generated declaration projection at the adapter boundary.</summary>
    /// <param name="resource">The inert authored resource.</param>
    /// <returns>Its original native declaration.</returns>
    public DeclaredResource Declaration(Resource resource) => _declarations[resource];
    internal void Bind(Resource resource, DeclaredResource declaration) => _declarations.Add(resource, declaration);
    internal void BindParameter(ParameterResource resource, DeclaredValue value) => _parameters.Add(resource, value);
    internal void BindConnection(Resource resource, ReferenceExpression connection) => _connections.Add(resource, connection);
    internal Task<DeclaredValue> Parameter(ParameterResource resource) => Task.FromResult(_parameters[resource]);
    /// <summary>Materializes symbolic syntax as native-owned handles, never resolved strings.</summary>
    /// <param name="expression">Recorded symbolic syntax.</param>
    /// <returns>Native value authority, not a payload.</returns>
    public Task<DeclaredValue> Value(ReferenceExpression expression)
    {
        if (!_values.TryGetValue(expression, out var value))
        {
            value = expression.Materialize(this);
            _values.Add(expression, value);
        }
 
        return value;
    }
    internal ReferenceExpression Connection(Resource resource) =>
        resource is IResourceWithConnectionString local ? local.ConnectionStringExpression : _connections[resource];
    /// <summary>Creates a reference to an already generated native value at a transport adapter boundary.</summary>
    /// <param name="value">Original delegated symbolic authority.</param>
    /// <returns>Recorded native symbolic syntax.</returns>
    public static ReferenceExpression Reference(DeclaredValue value) => new(_ => Task.FromResult(value));
    /// <summary>Records a connection supplied by native/provider materialization rather than authoring-time resolution.</summary>
    /// <param name="resource">The inert connection owner.</param>
    /// <returns>Deferred native connection syntax.</returns>
    public static ReferenceExpression ConnectionReference(Resource resource) => new(context => context.Value(context._connections[resource]));
}