| File: DotNet\DotNetAppHostRestorePreview.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 System.Globalization; using System.Xml.Linq; using Aspire.Cli.NuGet; using Aspire.Cli.Resources; using Aspire.Hosting.Utils; namespace Aspire.Cli.DotNet; /// <summary> /// Projects a configuration candidate into per-project native restore settings. /// </summary> internal sealed class DotNetAppHostRestorePreview : IDisposable { private readonly List<NuGetPackageOperationConfiguration> _configurations = []; private readonly TemporaryNuGetConfigFile _replacement; private FileInfo? _targetsFile; private DotNetAppHostRestorePreview(TemporaryNuGetConfigFile replacement) { _replacement = replacement; } internal FileInfo TargetsFile => _targetsFile ?? throw new InvalidOperationException("The restore preview has not been initialized."); internal NuGetPackageOperationConfiguration RootConfiguration => _configurations[0]; internal static async Task<DotNetAppHostRestorePreview> CreateAsync( BundleNuGetService nuGetService, DotNetAppHostNuGetConfigMergerCandidate candidate, DotNetRestoreSettings restoreSettings, FileInfo rootProject, IReadOnlyDictionary<string, string> projectRestoreTargets, CancellationToken cancellationToken) { if (!BundleNuGetService.IsAncestorDirectory(candidate.TargetFile.DirectoryName!, rootProject.DirectoryName!)) { throw new InvalidOperationException(string.Format( CultureInfo.CurrentCulture, UpdateCommandStrings.NuGetConfigOutsideHierarchyFormat, candidate.TargetFile.FullName, rootProject.DirectoryName)); } var replacement = await TemporaryNuGetConfigFile.CreatePreviewAsync( candidate.TargetFile, candidate.ProposedContent, cancellationToken); var preview = new DotNetAppHostRestorePreview(replacement); try { var target = new XElement("Target", new XAttribute("Name", "_AspireCandidateRestoreSettings"), new XAttribute("BeforeTargets", "_GetRestoreSettings")); var imports = new List<XElement>(); var customProjects = new List<string>(); // NuGetRestoreTargets is invocation-global. Dispatch the original per-project // imports so a referenced project's custom restore targets are not replaced. foreach (var (path, restoreTargets) in projectRestoreTargets) { if (string.IsNullOrEmpty(restoreTargets)) { continue; } var condition = ProjectCondition(path); customProjects.Add($"!({condition})"); imports.Add(new XElement("Import", new XAttribute("Project", MSBuildEscaping.Escape(Path.GetFullPath(restoreTargets, Path.GetDirectoryName(path)!))), new XAttribute("Condition", condition))); } imports.Add(new XElement("Import", new XAttribute("Project", MSBuildEscaping.Escape(restoreSettings.RestoreTargets)), new XAttribute("Condition", customProjects.Count == 0 ? "true" : string.Join(" And ", customProjects)))); foreach (var path in new[] { rootProject.FullName }.Concat(projectRestoreTargets.Keys).Distinct(StringComparers.FileSystemPath)) { // A selected ancestor config also affects children that inherit it. Evaluate its // replacement at native precedence, retaining every child's more-local policy. if (!BundleNuGetService.IsAncestorDirectory(candidate.TargetFile.DirectoryName!, Path.GetDirectoryName(path)!)) { continue; } var configuration = await nuGetService.CreateConfigurationPreviewAsync( new DirectoryInfo(Path.GetDirectoryName(path)!), candidate.TargetFile, replacement.ConfigFile, cancellationToken); preview._configurations.Add(configuration); var identity = string.Equals(path, rootProject.FullName, StringComparisons.FileSystemPath) ? restoreSettings.ProjectIdentity : path; target.Add(new XElement("PropertyGroup", new XAttribute("Condition", $"{ProjectCondition(identity)} And '$(RestoreConfigFile)' == '' And '$(RestoreRootConfigDirectory)' == ''"), new XElement("RestoreRootConfigDirectory", MSBuildEscaping.Escape(configuration.EffectiveWorkingDirectory.FullName)))); } var document = new XDocument(new XElement("Project", imports, target)); preview._targetsFile = new FileInfo(Path.Combine( preview.RootConfiguration.EffectiveWorkingDirectory.FullName, "Aspire.CandidateRestore.targets")); await File.WriteAllTextAsync(preview.TargetsFile.FullName, document.ToString(), cancellationToken); return preview; } catch { preview.Dispose(); throw; } } private static string ProjectCondition(string path) => $"('$(MSBuildProjectFullPath)' == '{MSBuildEscaping.Escape(path)}' Or '$(MSBuildProjectFullPath)' == '{MSBuildEscaping.Escape(PathNormalizer.ResolveToFilesystemPath(path))}')"; public void Dispose() { foreach (var configuration in _configurations) { configuration.Dispose(); } _replacement.Dispose(); } }