// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #nullable disable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Xml.Linq; using NuGet.Common; using NuGet.Frameworks; namespace NuGet.Test.Utility { public class ProjectFileUtils { public static void AddProperties(XDocument doc, Dictionary properties) { AddProperties(doc, properties, condition: null); } public static void AddProperties(XDocument doc, Dictionary properties, string condition) { var ns = doc.Root.GetDefaultNamespace(); var propertyGroup = new XElement(XName.Get("PropertyGroup", ns.NamespaceName)); foreach (var pair in properties) { var subItem = new XElement(XName.Get(pair.Key, ns.NamespaceName), pair.Value); AddCondition(condition, subItem); propertyGroup.Add(subItem); } var lastPropGroup = doc.Root.Elements().Last(e => e.Name.LocalName == "PropertyGroup"); lastPropGroup.AddAfterSelf(propertyGroup); } private static void AddCondition(string condition, XElement subItem) { if (MSBuildStringUtility.TrimAndGetNullForEmpty(condition) != null) { subItem.Add(new XAttribute(XName.Get("Condition"), condition)); } } public static void AddProperty(XDocument doc, string propertyName, string propertyValue) { AddProperty(doc, propertyName, propertyValue, condition: null); } public static void AddProperty(XDocument doc, string propertyName, string propertyValue, string condition) { var lastPropGroup = doc.Root.Elements().Last(e => e.Name.LocalName == "PropertyGroup"); var element = new XElement(XName.Get(propertyName), propertyValue); AddCondition(condition, element); lastPropGroup.Add(element); } public static void ChangeProperty(XDocument document, string propertyName, string newValue) { var property = document.Descendants(propertyName).Single(); property.Value = newValue; } public static bool HasCondition(XElement element, string condition) { var elementCondition = MSBuildStringUtility.TrimAndGetNullForEmpty(element.Attribute(XName.Get("Condition"))?.Value); return StringComparer.OrdinalIgnoreCase.Equals(MSBuildStringUtility.TrimAndGetNullForEmpty(condition), elementCondition); } /// /// Opens a project file, adds an MSBuild item, and writes it back. /// This is the simplest method for adding an item to a project file, but it is not the most efficient if you need to add multiple items since it opens and writes the file for each item added. /// Prefer this method whenever there you only need to do 1 item change. /// /// The full path to the project file. /// The MSBuild item type (e.g. PackageReference). /// The value of the Include attribute on the item element. /// /// The short TFM string used to generate a per-TFM condition on the ItemGroup. /// Pass an empty string to emit the item unconditionally. /// /// XML attributes to add on the item element itself (e.g. Version="1.0.0"). public static void AddItem(string projectFilePath, string name, string identity, string framework, Dictionary attributes) { using var stream = File.Open(projectFilePath, FileMode.Open, FileAccess.ReadWrite); var xml = XDocument.Load(stream); AddItem(xml, name, identity, framework, attributes); WriteXmlToFile(xml, stream); } /// /// Adds a new MSBuild item to the project file inside a new ItemGroup. /// When is a specific framework, a Condition attribute of the form /// '$(TargetFramework)' == '<tfm>' is added to the ItemGroup; otherwise no condition is emitted. /// Scenarios: /// /// Adding a PackageReference item /// Adding a PackageVersion item /// Adding a FrameworkReference item /// Adding a NuGetAuditSuppress item /// Adding a PrunePackageReference item /// /// /// The project XML document to modify. /// The MSBuild item type (e.g. PackageReference, FrameworkReference). /// The value of the Include attribute on the item element. /// /// The target framework used to generate a per-TFM condition on the ItemGroup. /// Pass (or any non-specific framework) to emit the item unconditionally. /// /// XML attributes to add on the item element itself (e.g. Version="1.0.0"). public static void AddItem(XDocument doc, string name, string identity, NuGetFramework framework, Dictionary attributes) { AddItem(doc, name, identity, framework?.IsSpecificFramework == true ? framework.GetShortFolderName() : string.Empty, attributes); } /// /// Adds a new MSBuild item to the project file inside a new ItemGroup. /// When is a non-empty string, a Condition attribute of the form /// '$(TargetFramework)' == '<tfm>' is added to the ItemGroup; pass an empty string to emit the item unconditionally. /// Item metadata is expressed as XML attributes on the item element. For most scenarios this overload is preferred over /// the one that accepts both properties and attributes. /// Scenarios: /// /// Adding a PackageReference item /// Adding a PackageVersion item /// Adding a FrameworkReference item /// Adding a NuGetAuditSuppress item /// Adding a PrunePackageReference item /// /// /// The project XML document to modify. /// The MSBuild item type (e.g. PackageReference, FrameworkReference). /// The value of the Include attribute on the item element. /// /// The short TFM string used to generate a per-TFM condition on the ItemGroup (e.g. "net8.0"). /// Pass an empty string to emit the item unconditionally. /// /// XML attributes to add on the item element itself (e.g. Version="1.0.0"). public static void AddItem(XDocument doc, string name, string identity, string framework, Dictionary attributes) { AddItem(doc, name, identity, framework, properties: [], attributes); } /// /// Adds a new MSBuild item to the project file inside a new ItemGroup. /// When is a non-empty string, a Condition attribute of the form /// '$(TargetFramework)' == '<tfm>' is added to the ItemGroup; pass an empty string to emit the item unconditionally. /// This overload accepts both (written as XML attributes on the item element) and /// (written as child XML elements). While MSBuild evaluates both equivalently as item metadata, /// prefer the overload that only takes for new call sites — it is more concise and matches /// the SDK-style project convention of expressing metadata as attributes. /// Use this overload only when a caller must distinguish between attribute-style and element-style metadata. /// /// Adding a PackageReference item /// Adding a PackageVersion item /// Adding a FrameworkReference item /// Adding a NuGetAuditSuppress item /// Adding a PrunePackageReference item /// /// /// The project XML document to modify. /// The MSBuild item type (e.g. PackageReference, FrameworkReference). /// The value of the Include attribute on the item element. /// /// The short TFM string used to generate a per-TFM condition on the ItemGroup (e.g. "net8.0"). /// Pass an empty string to emit the item unconditionally. /// /// Child XML elements to add under the item element (e.g. <Version>1.0.0</Version>). /// XML attributes to add on the item element itself (e.g. Version="1.0.0"). public static void AddItem(XDocument doc, string name, string identity, string framework, Dictionary properties, Dictionary attributes) { var ns = doc.Root.GetDefaultNamespace(); var itemGroup = new XElement(XName.Get("ItemGroup", ns.NamespaceName)); var entry = new XElement(XName.Get(name, ns.NamespaceName)); entry.Add(new XAttribute(XName.Get("Include"), identity)); itemGroup.Add(entry); if (!string.IsNullOrEmpty(framework)) { itemGroup.Add(new XAttribute(XName.Get("Condition"), $" '$(TargetFramework)' == '{framework}' ")); } foreach (var attribute in attributes) { var attr = new XAttribute(XName.Get(attribute.Key), attribute.Value); entry.Add(attr); } foreach (var pair in properties) { var subItem = new XElement(XName.Get(pair.Key, ns.NamespaceName), pair.Value); entry.Add(subItem); } var lastItemGroup = doc.Root.Elements().LastOrDefault(e => e.Name.LocalName == "ItemGroup"); if (lastItemGroup == null) { doc.Root.Elements().Last().AddAfterSelf(itemGroup); } else { lastItemGroup.AddAfterSelf(itemGroup); } } public static void AddCustomXmlToProjectRoot(XDocument doc, string xml) { var element = XElement.Parse(xml); doc.Root.Add(element); } public static void SetTargetFrameworkForProject(XDocument doc, string targetFrameworkPropertyName, string targetFrameworkValue) { var existingFrameworkProperty = "TargetFramework"; var pgElement = doc.Root.Descendants("PropertyGroup").FirstOrDefault(t => t.Descendants("TargetFramework").Any()); if (pgElement == null) { pgElement = doc.Root.Descendants("PropertyGroup").FirstOrDefault(t => t.Descendants("TargetFrameworks").Any()); existingFrameworkProperty = "TargetFrameworks"; } if (pgElement != null) { pgElement.SetElementValue(XName.Get(existingFrameworkProperty), null); pgElement.SetElementValue(XName.Get(targetFrameworkPropertyName), targetFrameworkValue); } else { AddProperty(doc, targetFrameworkPropertyName, targetFrameworkValue); } } public static void WriteXmlToFile(XDocument xml, FileStream stream) { var unicodeEncoding = new UTF8Encoding(); var xmlString = xml.ToString(); stream.SetLength(0); stream.Seek(0, SeekOrigin.Begin); stream.Write(unicodeEncoding.GetBytes(xmlString), 0, unicodeEncoding.GetByteCount(xmlString)); } } }