File: Publishing\Constructs\RadiusContainerConstruct.cs
Web Access
Project: src\src\Aspire.Hosting.Radius\Aspire.Hosting.Radius.csproj (Aspire.Hosting.Radius)
// 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 ASPIRERADIUS004 // Experimental: ConfigureRadiusInfrastructure escape-hatch construct types are consumed internally by the publisher.
 
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.Radius.ResourceMapping;
using Azure.Provisioning;
using Azure.Provisioning.Primitives;
 
namespace Aspire.Hosting.Radius.Publishing.Constructs;
 
/// <summary>
/// Represents a <c>Radius.Compute/containers</c> resource in the Bicep AST.
/// </summary>
/// <remarks>
/// Aligned with the Radius container v2 schema (<c>Radius.Compute/containers@2025-08-01-preview</c>).
/// The <c>imagePullPolicy</c> property has been removed from the v2 schema.
/// See: https://github.com/radius-project/radius/blob/main/eng/design-notes/extensibility/2025-08-container-resource-type.md
/// </remarks>
[Experimental("ASPIRERADIUS004", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class RadiusContainerConstruct : ProvisionableResource
{
    private readonly string _containerName;
    private BicepValue<string>? _name;
    private BicepValue<string>? _image;
    private BicepValue<string>? _applicationId;
    private BicepValue<string>? _environmentId;
    private BicepDictionary<ConnectionConstruct>? _connections;
    private BicepDictionary<ContainerEnvVarConstruct>? _env;
    private BicepDictionary<ContainerPortConstruct>? _ports;
 
    /// <summary>The resource name.</summary>
    public BicepValue<string> ContainerName
    {
        get { Initialize(); return _name!; }
        set { Initialize(); _name!.Assign(value); }
    }
 
    /// <summary>
    /// The immutable <c>properties.containers</c> map key captured at construction (the Aspire
    /// resource name). The Radius v2 schema and recipe do <em>not</em> require this to equal the
    /// top-level <c>name:</c> (<see cref="ContainerName"/>) — Radius permits distinct names. Aspire
    /// requires them to match because it derives service discovery from the resource name, and the
    /// publisher validates that invariant after running <c>ConfigureRadiusInfrastructure</c>
    /// callbacks so service discovery cannot silently disagree with the generated Service name.
    /// </summary>
    internal string ContainerMapKey => _containerName;
 
    /// <summary>Container image (e.g., "nginx:latest").</summary>
    public BicepValue<string> Image
    {
        get { Initialize(); return _image!; }
        set { Initialize(); _image!.Assign(value); }
    }
 
    /// <summary>Reference to the application resource ID.</summary>
    public BicepValue<string> ApplicationId
    {
        get { Initialize(); return _applicationId!; }
        set { Initialize(); _applicationId!.Assign(value); }
    }
 
    /// <summary>Reference to the environment resource ID.</summary>
    public BicepValue<string> EnvironmentId
    {
        get { Initialize(); return _environmentId!; }
        set { Initialize(); _environmentId!.Assign(value); }
    }
 
    /// <summary>
    /// Dictionary of named connections to other resources.
    /// Keys are connection names; values contain source resource ID references.
    /// </summary>
    public BicepDictionary<ConnectionConstruct> Connections
    {
        get { Initialize(); return _connections!; }
        set { Initialize(); _connections!.Assign(value); }
    }
 
    /// <summary>
    /// Environment variables for the container, keyed by variable name. Each entry carries
    /// a <c>value</c> (a literal or a reference to a Bicep parameter for secret values).
    /// </summary>
    public BicepDictionary<ContainerEnvVarConstruct> Env
    {
        get { Initialize(); return _env!; }
        set { Initialize(); _env!.Assign(value); }
    }
 
    /// <summary>
    /// Ports exposed by the container, keyed by port name. Each entry carries a
    /// <c>containerPort</c> and an optional <c>protocol</c>.
    /// </summary>
    public BicepDictionary<ContainerPortConstruct> Ports
    {
        get { Initialize(); return _ports!; }
        set { Initialize(); _ports!.Assign(value); }
    }
 
    /// <summary>
    /// Initializes a new <see cref="RadiusContainerConstruct"/> with the given Bicep
    /// identifier and Radius container resource name.
    /// </summary>
    /// <param name="bicepIdentifier">
    /// The Bicep identifier for this resource. May be sanitized (e.g., hyphens become
    /// underscores) so it remains a valid C#/Bicep identifier.
    /// </param>
    /// <param name="containerName">
    /// The Radius container resource name as it should appear in the deployed manifest.
    /// This value is used as the map key under <c>properties.containers</c>. The Radius v2
    /// schema does not require this map key to equal the top-level <c>name:</c>, but Aspire
    /// keys both by the resource name and requires them to match so the service discovery it
    /// derives from that name resolves to the Service the recipe creates (see
    /// <see cref="ContainerMapKey"/>).
    /// It must be the unsanitized resource name (hyphens preserved); using
    /// <c>BicepIdentifier</c> here would emit a key that does not match the <c>name</c>
    /// when the resource name contains characters that get sanitized.
    /// </param>
    public RadiusContainerConstruct(string bicepIdentifier, string containerName)
        : base(bicepIdentifier, new Azure.Core.ResourceType(RadiusResourceTypes.Containers), RadiusResourceTypes.RadiusApiVersion)
    {
        ArgumentException.ThrowIfNullOrEmpty(containerName);
        _containerName = containerName;
    }
 
    /// <inheritdoc />
    protected override void DefineProvisionableProperties()
    {
        _name = DefineProperty<string>(nameof(ContainerName), ["name"]);
        // The container v2 schema keys `properties.containers` by the resource name (the
        // value emitted at `name:`), not by the Bicep identifier. Using BicepIdentifier
        // here would (a) snapshot the pre-rename identifier if a ConfigureRadiusInfrastructure
        // callback renamed it, and (b) emit a key with sanitized characters (hyphens →
        // underscores) that no longer matches `name`.
        _image = DefineProperty<string>(nameof(Image), ["properties", "containers", _containerName, "image"]);
        _applicationId = DefineProperty<string>(nameof(ApplicationId), ["properties", "application"]);
        // The Radius.Compute/containers v2 schema requires an explicit environment reference
        // so the control plane can resolve the recipe pack that provisions the container.
        _environmentId = DefineProperty<string>(nameof(EnvironmentId), ["properties", "environment"]);
        _connections = DefineDictionaryProperty<ConnectionConstruct>(nameof(Connections), ["properties", "connections"]);
        // env and ports live inside the per-container object under `properties.containers.<name>`,
        // keyed (like image) by the resource name so the map key matches the emitted `name:`.
        _env = DefineDictionaryProperty<ContainerEnvVarConstruct>(nameof(Env), ["properties", "containers", _containerName, "env"]);
        _ports = DefineDictionaryProperty<ContainerPortConstruct>(nameof(Ports), ["properties", "containers", _containerName, "ports"]);
    }
}