File: Msi\WixDocument.cs
Web Access
Project: Microsoft.DotNet.Build.Tasks.Workloads.csproj (Microsoft.DotNet.Build.Tasks.Workloads)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Xml.Linq;

namespace Microsoft.DotNet.Build.Tasks.Workloads.Msi;

/// <summary>
/// Class for loading and modifying existing WiX XML source files to support compositional authoring.
/// </summary>
public class WixDocument 
{
    private static readonly XNamespace s_wixNamespace = "http://wixtoolset.org/schemas/v4/wxs";

    private XDocument _doc;

    private string _path;

    public static string GetDirectoryReference()
        => $"dir{Guid.NewGuid():N}";

    public XElement Package => _doc.Root.Descendants(s_wixNamespace + "Package").FirstOrDefault();

    /// <summary>
    /// Creates a new instance of the <see cref="WixDocument"/> class by loading an existing WiX XML 
    /// document from the specified file path.
    /// </summary>
    /// <param name="path">The path of the WiX XML document.</param>
    public WixDocument(string path)
    {
        _doc = XDocument.Load(path);
        _path = path;
    }

    /// <summary>
    /// Save the current current state of the WiX XML document to the orignal file path.
    /// </summary>
    public void Save()
    {
        _doc.Save(_path);
    }

    /// <summary>
    /// Gets the first Directory element with matching Id attribute.
    /// </summary>
    /// <param name="id">The directory identifier to match.</param>
    /// <returns>The first matching Directory element or null if no elements exist.</returns>
    public XElement GetDirectory(string id) =>
        GetElement("Directory", id);

    /// <summary>
    /// Searches the underlying document for the first element matching the provided name and ID.
    /// </summary>
    /// <param name="elementName">The name of the element to find.</param>
    /// <param name="id">The Id attribute of the element to match. If null, the first matching element is returned.</param>
    /// <param name="ns">Optional namespace to use. If null, the default WiX namespace is used.</param>
    /// <returns>The element or null if it was not found.</returns>
    public XElement GetElement(string elementName, string id = null, XNamespace ns = null)
    {
        if (string.IsNullOrWhiteSpace(id))
        {
            return _doc.Root.Descendants((ns ?? s_wixNamespace) + elementName).FirstOrDefault();
        }

        foreach (XElement element in _doc.Root.Descendants((ns ?? s_wixNamespace) + elementName))
        {
            if (element.Attribute("Id")?.Value == id)
            {
                return element;
            }
        }

        return null;
    }

    /// <summary>
    /// Gets the first Feature element with matching Id attribute.
    /// </summary>
    /// <param name="id">The feature identifier to match</param>
    /// <returns>The Feature element or null if it does not exist.</returns>
    public XElement GetFeature(string id) =>
        GetElement("Feature", id);

    /// <summary>
    /// Adds a RegistryKey element to the specified component.
    /// </summary>
    /// <param name="componentId">The identifier of the component.</param>
    /// <param name="registryKey">The RegistryKey element to add.</param>
    /// <exception cref="InvalidOperationException" />
    public void AddRegistryKey(string componentId, XElement registryKey)
    {
        var component = GetElement("Component", componentId) ??
            throw new InvalidOperationException($"The specified component does not exist: {componentId}");
        component.Add(registryKey);
    }

    /// <summary>
    /// Adds a Property element to the Package.
    /// </summary>
    /// <param name="id">The property identifier.</param>
    /// <param name="value"></param>
    public void AddProperty(string id, string value) =>
        Package.Add(new XElement(s_wixNamespace + "Property",
            new XAttribute("Id", id),
            new XAttribute("Value", value)));

    /// <summary>
    /// Adds a PropertyRef element to the Package.
    /// </summary>
    /// <param name="id"></param>
    public void AddPropertyRef(string id) =>
        Package.Add(new XElement(s_wixNamespace + "PropertyRef",
            new XAttribute("Id", id)));

    /// <summary>
    /// Adds a CustomActionRef element to the Package.
    /// </summary>
    /// <param name="id"></param>
    public void AddCustomActionRef(string id) =>
        Package.Add(new XElement(s_wixNamespace + "CustomActionRef",
            new XAttribute("Id", id)));

    /// <summary>
    /// Creates a directory element with the provided name and unique identifier.
    /// </summary>
    /// <param name="name">The name of the directory.</param>
    /// <returns>A new element representing the directory.</returns>
    public static XElement CreateDirectory(string name) =>
        CreateDirectory(name, $"dir{Guid.NewGuid():N}");

    /// <summary>
    /// Creates a directory element with the provided name and identifier.
    /// </summary>
    /// <param name="name">The name of the directory.</param>
    /// <param name="id">The identifier for the directory element. The identifier must be unique within the installer.</param>
    /// <returns>A new element representing the directory.</returns>
    public static XElement CreateDirectory(string name, string id) =>
         new XElement(s_wixNamespace + "Directory",
            new XAttribute("Id", id),
            new XAttribute("Name", name));

    /// <summary>
    /// Creates a RegistryKey element with the specified key path and root hive. The new element can be added as a child to any existing Component or RegistryKey element.
    /// </summary>
    /// <param name="key">The name of the registry key.</param>
    /// <param name="root">The registry key (HKLM, HKCR, etc.).</param>
    /// <returns></returns>
    public static XElement CreateRegistryKey(string key, string root = "HKLM") =>
        new XElement(s_wixNamespace + "RegistryKey",
            new XAttribute("Root", root),
            new XAttribute("Key", key));
}