File: Dcp\DcpNameGenerator.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 System.Collections.Immutable;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Dcp;
 
internal sealed class DcpNameGenerator
{
    // A random suffix added to every DCP object name ensures that those names (and derived object names, for example container names)
    // are unique machine-wide with a high level of probability.
    // The length of 8 achieves that while keeping the names relatively short and readable.
    // The second purpose of the suffix is to play a role of a unique OpenTelemetry service instance ID.
    private const int RandomNameSuffixLength = 8;
    private readonly IConfiguration _configuration;
    private readonly IOptions<DcpOptions> _options;
 
    public DcpNameGenerator(IConfiguration configuration, IOptions<DcpOptions> options)
    {
        _configuration = configuration;
        _options = options;
    }
 
    public void EnsureDcpInstancesPopulated(IResource resource)
    {
        if (resource.TryGetLastAnnotation<DcpInstancesAnnotation>(out _))
        {
            return;
        }
 
        if (resource.IsContainer())
        {
            var (name, suffix) = GetContainerName(resource);
            AddInstancesAnnotation(resource, [new DcpInstance(name, suffix, 0)]);
        }
        else if (resource is ExecutableResource)
        {
            var (name, suffix) = GetExecutableName(resource);
            AddInstancesAnnotation(resource, [new DcpInstance(name, suffix, 0)]);
        }
        else if (resource is ProjectResource)
        {
            var replicas = resource.GetReplicaCount();
            var builder = ImmutableArray.CreateBuilder<DcpInstance>(replicas);
            for (var i = 0; i < replicas; i++)
            {
                var (name, suffix) = GetExecutableName(resource);
                builder.Add(new DcpInstance(name, suffix, i));
            }
            AddInstancesAnnotation(resource, builder.ToImmutable());
        }
    }
 
    private static void AddInstancesAnnotation(IResource resource, ImmutableArray<DcpInstance> instances)
    {
        resource.Annotations.Add(new DcpInstancesAnnotation(instances));
    }
 
    public (string Name, string Suffix) GetContainerName(IResource container)
    {
        var nameSuffix = container.GetContainerLifetimeType() switch
        {
            ContainerLifetime.Session => GetRandomNameSuffix(),
            // Compute a short hash of the content root path to differentiate between multiple AppHost projects with similar resource names
            _ => _configuration["AppHost:Sha256"]!.Substring(0, RandomNameSuffixLength).ToLowerInvariant(),
        };
 
        return (GetObjectNameForResource(container, _options.Value, nameSuffix), nameSuffix);
    }
 
    public (string Name, string Suffix) GetExecutableName(IResource project)
    {
        var nameSuffix = GetRandomNameSuffix();
        return (GetObjectNameForResource(project, _options.Value, nameSuffix), nameSuffix);
    }
 
    private static string GetRandomNameSuffix()
    {
        // RandomNameSuffixLength of lowercase characters
        var suffix = PasswordGenerator.Generate(RandomNameSuffixLength, true, false, false, false, RandomNameSuffixLength, 0, 0, 0);
        return suffix;
    }
 
    private static string GetObjectNameForResource(IResource resource, DcpOptions options, string suffix = "")
    {
        if (resource.TryGetLastAnnotation<ContainerNameAnnotation>(out var containerNameAnnotation))
        {
            // If an explicit container name is provided, use it without any postfix
            return containerNameAnnotation.Name;
        }
 
        static string maybeWithSuffix(string s, string localSuffix, string? globalSuffix)
            => (string.IsNullOrWhiteSpace(localSuffix), string.IsNullOrWhiteSpace(globalSuffix)) switch
            {
                (true, true) => s,
                (false, true) => $"{s}-{localSuffix}",
                (true, false) => $"{s}-{globalSuffix}",
                (false, false) => $"{s}-{localSuffix}-{globalSuffix}"
            };
        return maybeWithSuffix(resource.Name, suffix, options.ResourceNameSuffix);
    }
}