File: AzureManagedRedisResource.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.Redis\Aspire.Hosting.Azure.Redis.csproj (Aspire.Hosting.Azure.Redis)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
using Azure.Provisioning;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.Primitives;
using Azure.Provisioning.RedisEnterprise;
 
namespace Aspire.Hosting.Azure;
 
/// <summary>
/// Represents an Azure Managed Redis resource.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="configureInfrastructure">Callback to configure the Azure resources.</param>
public class AzureManagedRedisResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
    : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString
{
    /// <summary>
    /// Gets the "connectionString" output reference from the bicep template for the Azure Managed Redis resource.
    ///
    /// This is used when Entra ID authentication is used. The connection string is an output of the bicep template.
    /// </summary>
    private BicepOutputReference ConnectionStringOutput => new("connectionString", this);
 
    /// <summary>
    /// Gets the "connectionString" secret reference from the key vault associated with this resource.
    ///
    /// This is set when access key authentication is used. The connection string is stored in a secret in the Azure Key Vault.
    /// </summary>
    internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; }
 
    /// <summary>
    /// Gets the "primaryAccessKey" secret reference from the key vault associated with this resource.
    ///
    /// This is set when access key authentication is used. The primary access key is stored in a secret in the Azure Key Vault.
    /// </summary>
    internal IAzureKeyVaultSecretReference? PrimaryAccessKeySecretOutput { get; set; }
 
    /// <summary>
    /// Gets the "name" output reference for the resource.
    /// </summary>
    public BicepOutputReference NameOutputReference => new("name", this);
 
    /// <summary>
    /// Gets the "hostName" output reference from the bicep template for the Azure Redis resource.
    /// </summary>
    private BicepOutputReference HostNameOutput => new("hostName", this);
 
    /// <summary>
    /// Gets a value indicating whether the resource uses access key authentication.
    /// </summary>
    [MemberNotNullWhen(true, nameof(ConnectionStringSecretOutput))]
    [MemberNotNullWhen(true, nameof(PrimaryAccessKeySecretOutput))]
    public bool UseAccessKeyAuthentication => ConnectionStringSecretOutput is not null;
 
    /// <summary>
    /// Gets the inner Redis resource.
    /// 
    /// This is set when RunAsContainer is called on the AzureManagedRedisResource resource to create a local Redis container.
    /// </summary>
    internal RedisResource? InnerResource { get; private set; }
 
    /// <inheritdoc />
    public override ResourceAnnotationCollection Annotations => InnerResource?.Annotations ?? base.Annotations;
 
    /// <summary>
    /// Gets the connection string template for the manifest for the Azure Managed Redis resource.
    /// </summary>
    public ReferenceExpression ConnectionStringExpression =>
        InnerResource?.ConnectionStringExpression ??
            (UseAccessKeyAuthentication ?
                ReferenceExpression.Create($"{ConnectionStringSecretOutput}") :
                ReferenceExpression.Create($"{ConnectionStringOutput}"));
 
    /// <summary>
    /// Gets the host name for the Redis server.
    /// </summary>
    /// <remarks>
    /// In container mode, resolves to the container's primary endpoint host and port.
    /// In Azure mode, resolves to the Azure Redis server's hostname.
    /// </remarks>
    public ReferenceExpression HostName =>
        InnerResource is not null ?
            ReferenceExpression.Create($"{InnerResource.PrimaryEndpoint.Property(EndpointProperty.Host)}") :
            ReferenceExpression.Create($"{HostNameOutput}");
 
    /// <summary>
    /// Gets the port for the Redis server.
    /// </summary>
    /// <remarks>
    /// In container mode, resolves to the container's primary endpoint port.
    /// In Azure mode, resolves to 10000.
    /// </remarks>
    public ReferenceExpression Port =>
        InnerResource is not null ?
            ReferenceExpression.Create($"{InnerResource.Port}") :
            ReferenceExpression.Create($"10000"); // Based on the ConfigureRedisInfrastructure method
 
    /// <summary>
    /// Gets the password/access key for the Redis server.
    /// </summary>
    /// <remarks>
    /// <list type="bullet">
    /// <item>When running as a container, returns the password parameter value if one exists.</item>
    /// <item>When using access key authentication in Azure mode, returns the primary access key from KeyVault.</item>
    /// <item>When using Entra ID authentication in Azure mode, returns an empty expression.</item>
    /// </list>
    /// </remarks>
    public ReferenceExpression? Password =>
        InnerResource is not null ?
            (InnerResource.PasswordParameter is not null ?
                ReferenceExpression.Create($"{InnerResource.PasswordParameter}") :
                null) :
            UseAccessKeyAuthentication ?
                ReferenceExpression.Create($"{PrimaryAccessKeySecretOutput}") :
                null;
 
    /// <summary>
    /// Gets the connection URI expression for the Redis server.
    /// </summary>
    /// <remarks>
    /// Format: <c>redis://[:{password}@]{host}:{port}</c>. The password segment is omitted when using Entra ID authentication in Azure mode.
    /// </remarks>
    public ReferenceExpression UriExpression
    {
        get
        {
            if (InnerResource is not null)
            {
                return InnerResource.UriExpression;
            }
 
            if (UseAccessKeyAuthentication)
            {
                return ReferenceExpression.Create($"rediss://:{PrimaryAccessKeySecretOutput:uri}@{HostName}:{Port}");
            }
 
            return ReferenceExpression.Create($"rediss://{HostName}:{Port}");
        }
    }
 
    internal void SetInnerResource(RedisResource innerResource)
    {
        // Copy the annotations to the inner resource before making it the inner resource
        foreach (var annotation in Annotations)
        {
            innerResource.Annotations.Add(annotation);
        }
 
        InnerResource = innerResource;
    }
 
    /// <inheritdoc/>
    public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra)
    {
        var bicepIdentifier = this.GetBicepIdentifier();
        var resources = infra.GetProvisionableResources();
 
        // Check if a RedisEnterpriseCluster with the same identifier already exists
        var existingCluster = resources.OfType<RedisEnterpriseCluster>().SingleOrDefault(cluster => cluster.BicepIdentifier == bicepIdentifier);
 
        if (existingCluster is not null)
        {
            return existingCluster;
        }
 
        // Create and add new resource if it doesn't exist
        var cluster = RedisEnterpriseCluster.FromExisting(bicepIdentifier);
 
        if (!TryApplyExistingResourceAnnotation(
            this,
            infra,
            cluster))
        {
            cluster.Name = NameOutputReference.AsProvisioningParameter(infra);
        }
 
        infra.Add(cluster);
        return cluster;
    }
 
    /// <inheritdoc/>
    public override void AddRoleAssignments(IAddRoleAssignmentsContext roleAssignmentContext)
    {
        Debug.Assert(!UseAccessKeyAuthentication, "AddRoleAssignments should not be called when using AccessKeyAuthentication");
 
        var infra = roleAssignmentContext.Infrastructure;
        var redisEnterprise = (RedisEnterpriseCluster)AddAsExistingResource(infra);
 
        var redisEnterpriseDatabase = infra.GetProvisionableResources()
            .OfType<RedisEnterpriseDatabase>()
            .SingleOrDefault(db => db.BicepIdentifier == redisEnterprise.BicepIdentifier + "_default");
        if (redisEnterpriseDatabase is null)
        {
            redisEnterpriseDatabase = RedisEnterpriseDatabase.FromExisting(redisEnterprise.BicepIdentifier + "_default");
            redisEnterpriseDatabase.Name = "default";
            redisEnterpriseDatabase.Parent = redisEnterprise;
            infra.Add(redisEnterpriseDatabase);
        }
 
        var principalId = roleAssignmentContext.PrincipalId;
 
        AddEnterpriseContributorPolicyAssignment(infra, redisEnterpriseDatabase, principalId);
    }
 
    private static void AddEnterpriseContributorPolicyAssignment(AzureResourceInfrastructure infra, RedisEnterpriseDatabase database, BicepValue<Guid> principalId)
    {
        // For Redis Enterprise, we need to use the appropriate access policy assignment
        // This may need to be adjusted based on the actual Azure.Provisioning.Redis types available
        infra.Add(new AccessPolicyAssignment($"{database.BicepIdentifier}_contributor")
        {
            Name = BicepFunction.CreateGuid(database.Id, principalId, "default"),
            Parent = database,
            AccessPolicyName = "default",
            UserObjectId = principalId
        });
    }
 
    IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties()
    {
        if (InnerResource is not null)
        {
            foreach (var property in ((IResourceWithConnectionString)InnerResource).GetConnectionProperties())
            {
                yield return property;
            }
            yield break;
        }
 
        yield return new("Host", HostName);
        yield return new("Port", Port);
        yield return new("Uri", UriExpression);
        
        if (Password is not null)
        {
            yield return new("Password", Password);
        }
    }
}