File: DotNet\DotNetAppHostNuGetConfigBuilder.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.NuGet;
using Aspire.Cli.Packaging;
 
namespace Aspire.Cli.DotNet;
 
internal static class DotNetAppHostNuGetConfigBuilder
{
    public static NuGetConfiguration Build(
        NuGetSettingsInfo settings, string workloadId, PackageMapping[]? selectedMappings)
    {
        var operation = NuGetConfigurationBuilder.Build(
            settings, workloadId, selectedMappings, restrictToSelectedSources: false,
            packageScopedAppendSource: null, hasAuthoritativeAspirePolicy: true);
 
        return new NuGetConfiguration(settings, operation.ConfigSources, operation.Overlay)
        {
            AppHostConfig = selectedMappings is null
                ? null
                : Compose(settings, workloadId, selectedMappings, operation.ConfigSources, preservedSources: [])
        };
    }
 
    public static NuGetConfiguration BuildStable(
        NuGetSettingsInfo settings, string workloadId, PackageChannel channel)
    {
        var operation = NuGetConfigurationBuilder.BuildStableAppHostConfiguration(settings, workloadId, channel);
 
        return new NuGetConfiguration(settings, operation.ConfigSources, operation.Overlay)
        {
            AppHostConfig = Compose(settings, workloadId, selectedMappings: [], selectedSources: [],
                [.. (channel.Mappings ?? []).Select(static mapping => mapping.Source), PackageSources.NuGetOrg])
        };
    }
 
    private static DotNetAppHostNuGetConfig Compose(
        NuGetSettingsInfo settings,
        string workloadId,
        IReadOnlyList<PackageMapping> selectedMappings,
        IReadOnlyList<NuGetConfigSource> selectedSources,
        IReadOnlyList<string> preservedSources)
    {
        var integrationMappings = selectedMappings
            .Where(static mapping => mapping.PackageFilter != PackageMapping.AllPackages)
            .ToArray();
        var integrationSources = selectedSources
            .Where(source => integrationMappings.Any(mapping =>
                PackageSourceIdentity.Comparer.Equals(mapping.Source, source.Source)))
            .ToArray();
        // Introducing the first mapping makes every unmapped dependency ineligible for
        // restore. Keep mapping disabled rather than persisting ambient fallback policy.
        // https://learn.microsoft.com/nuget/consume-packages/package-source-mapping#package-pattern-requirements
        var mappings = settings.PackageSourceMappings.Count == 0
            ? []
            : integrationSources.Select(source => new NuGetPackageSourceMapping(source.Key,
                [.. integrationMappings
                    .Where(mapping => PackageSourceIdentity.Comparer.Equals(mapping.Source, source.Source))
                    .Select(static mapping => mapping.PackageFilter)
                    .Distinct(StringComparer.OrdinalIgnoreCase)]))
                .ToArray();
        var selectedPatterns = selectedMappings.Count == 0
            ? [PackageSourceOverrideMappings.DefaultPackagePattern]
            : integrationMappings.Select(static mapping => mapping.PackageFilter)
                .Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
        var selectedKeys = selectedSources.Select(static source => source.Key)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        var preservedIdentities = preservedSources
            .Select(source => NuGetSourceIdentity.Compute(source, settings.SourceIdentityKey))
            .ToHashSet(StringComparer.Ordinal);
        var retiredKeys = new List<string>();
        var removedMappings = new List<NuGetPackageSourceMapping>();
        // Only CLI-owned channel entries are maintained. User mappings, including exact
        // Aspire package mappings, retain their native NuGet precedence.
        foreach (var source in settings.Sources.Where(source =>
            NuGetConfigurationBuilder.IsCliManagedSource(source, workloadId) &&
            !preservedIdentities.Contains(source.Identity)))
        {
            var patterns = settings.PackageSourceMappings
                .Where(mapping => string.Equals(mapping.SourceKey, source.Name, StringComparison.OrdinalIgnoreCase))
                .SelectMany(static mapping => mapping.Patterns)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();
            var obsoletePatterns = patterns.Where(pattern =>
                (selectedMappings.Count == 0 && pattern == PackageMapping.AllPackages ||
                 selectedPatterns.Any(selectedPattern =>
                     PackageSourceOverrideMappings.CompetesWithAuthoritativePattern(pattern, selectedPattern))) &&
                !mappings.Any(selected =>
                    string.Equals(selected.SourceKey, source.Name, StringComparison.OrdinalIgnoreCase) &&
                    selected.Patterns.Contains(pattern, StringComparer.OrdinalIgnoreCase))).ToArray();
            if (!selectedKeys.Contains(source.Name) &&
                patterns.Except(obsoletePatterns, StringComparer.OrdinalIgnoreCase)
                    .All(static pattern => pattern == PackageMapping.AllPackages))
            {
                retiredKeys.Add(source.Name);
            }
            else if (obsoletePatterns.Length > 0)
            {
                removedMappings.Add(new NuGetPackageSourceMapping(source.Name, obsoletePatterns));
            }
        }
        var sourceChanges = new NuGetConfigOverlay(
            Sources: [.. integrationSources.Where(static source => !source.IsAmbient)
                .Select(static source => (source.Key, source.Source))],
            PackageSourceMappings: [],
            ClearDisabledPackageSources: false,
            DisabledPackageSourceKeys: [],
            GlobalPackagesFolder: null)
        {
            RetiredSourceKeys = retiredKeys
        };
 
        return new DotNetAppHostNuGetConfig(
            workloadId, sourceChanges, [.. integrationSources.Select(static source => source.Key)], mappings, removedMappings);
    }
}
 
internal sealed record DotNetAppHostNuGetConfig(
    string WorkloadId,
    NuGetConfigOverlay SourceChanges,
    IReadOnlyList<string> SelectedSourceKeys,
    IReadOnlyList<NuGetPackageSourceMapping> PackageSourceMappings,
    IReadOnlyList<NuGetPackageSourceMapping> RemovedPackageSourceMappings);