File: ManifestUtil\Manifest.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// 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.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
 
#nullable disable
 
namespace Microsoft.Build.Tasks.Deployment.ManifestUtilities
{
    /// <summary>
    /// Describes base functionality common to all supported manifest types.
    /// </summary>
    [ComVisible(false)]
    public abstract class Manifest
    {
        private AssemblyIdentity _assemblyIdentity;
        private AssemblyReference[] _assemblyReferences;
        private string _description;
        private FileReference[] _fileReferences;
        private string _sourcePath;
        private Stream _inputStream;
        private FileReferenceCollection _fileReferenceList;
        private AssemblyReferenceCollection _assemblyReferenceList;
        private readonly OutputMessageCollection _outputMessages = new OutputMessageCollection();
        private bool _treatUnfoundNativeAssembliesAsPrerequisites;
        private bool _readOnly;
 
        protected internal Manifest() // only internal classes can extend this class
        {
        }
 
        /// <summary>
        /// Specifies the identity of the manifest.
        /// </summary>
        [XmlIgnore]
        public AssemblyIdentity AssemblyIdentity
        {
            get => _assemblyIdentity ?? (_assemblyIdentity = new AssemblyIdentity());
            set => _assemblyIdentity = value;
        }
 
        /// <summary>
        /// Specifies the set of assemblies referenced by the manifest.
        /// </summary>
        [XmlIgnore]
        public AssemblyReferenceCollection AssemblyReferences => _assemblyReferenceList ??
                                                                 (_assemblyReferenceList = new AssemblyReferenceCollection(_assemblyReferences));
 
        private void CollectionToArray()
        {
            if (_assemblyReferenceList != null)
            {
                _assemblyReferences = _assemblyReferenceList.ToArray();
                _assemblyReferenceList = null;
            }
            if (_fileReferenceList != null)
            {
                _fileReferences = _fileReferenceList.ToArray();
                _fileReferenceList = null;
            }
        }
 
        /// <summary>
        /// Assembly name passed to the manifest generation task
        /// </summary>
        [XmlIgnore]
        public string AssemblyName { get; set; }
 
        /// <summary>
        /// Indicates if manifest is part of Launcher-based deployment, which requires
        /// somewhat different manifest generation and validation.
        /// </summary>
        [XmlIgnore]
        public bool LauncherBasedDeployment { get; set; } = false;
 
        /// <summary>
        /// Specifies a textual description for the manifest.
        /// </summary>
        [XmlIgnore]
        public string Description
        {
            get => _description;
            set => _description = value;
        }
 
        /// <summary>
        /// Identifies an assembly reference which is the entry point of the application.
        /// </summary>
        [XmlIgnore]
        public virtual AssemblyReference EntryPoint
        {
            get { return null; }
            set { }
        }
 
        /// <summary>
        /// Specifies the set of files referenced by the manifest.
        /// </summary>
        [XmlIgnore]
        public FileReferenceCollection FileReferences => _fileReferenceList ?? (_fileReferenceList = new FileReferenceCollection(_fileReferences));
 
        /// <summary>
        /// The input stream from which the manifest was read.
        /// Used by ManifestWriter to reconstitute input which is not represented in the object representation.
        /// </summary>
        [XmlIgnore]
        public Stream InputStream
        {
            get => _inputStream;
            set => _inputStream = value;
        }
 
        internal virtual void OnAfterLoad()
        {
        }
 
        internal virtual void OnBeforeSave()
        {
            CollectionToArray();
            SortFiles();
        }
 
        /// <summary>
        /// Contains a collection of current error and warning messages.
        /// </summary>
        [XmlIgnore]
        public OutputMessageCollection OutputMessages => _outputMessages;
 
        /// <summary>
        /// Specifies whether the manifest is operating in read-only or read-write mode.
        /// If only using to read a manifest then set this flag to true.
        /// If using to write a new manifest then set this flag to false.
        /// The default is false.
        /// This flag provides additional context for the manifest generator, and affects how some error messages are reported.
        /// </summary>
        [XmlIgnore]
        public bool ReadOnly
        {
            get => _readOnly;
            set => _readOnly = value;
        }
 
        private static bool ResolveAssembly(AssemblyReference a, string[] searchPaths)
        {
            if (a == null)
            {
                return false;
            }
 
            a.ResolvedPath = ResolvePath(a.SourcePath, searchPaths);
            if (!String.IsNullOrEmpty(a.ResolvedPath))
            {
                return true;
            }
 
            if (a.AssemblyIdentity != null)
            {
                a.ResolvedPath = a.AssemblyIdentity.Resolve(searchPaths);
                if (!String.IsNullOrEmpty(a.ResolvedPath))
                {
                    return true;
                }
            }
 
            a.ResolvedPath = ResolvePath(a.TargetPath, searchPaths);
            if (!String.IsNullOrEmpty(a.ResolvedPath))
            {
                return true;
            }
 
            return false;
        }
 
        private static bool ResolveFile(BaseReference f, string[] searchPaths)
        {
            if (f == null)
            {
                return false;
            }
 
            f.ResolvedPath = ResolvePath(f.SourcePath, searchPaths);
            if (!String.IsNullOrEmpty(f.ResolvedPath))
            {
                return true;
            }
 
            f.ResolvedPath = ResolvePath(f.TargetPath, searchPaths);
            if (!String.IsNullOrEmpty(f.ResolvedPath))
            {
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Locates all specified assembly and file references by searching in the same directory as the loaded manifest, or in the current directory.
        /// The location of each referenced assembly and file is required for hash computation and assembly identity resolution.
        /// Any resulting errors or warnings are reported in the OutputMessages collection.
        /// </summary>
        public void ResolveFiles()
        {
            string defaultDir = String.Empty;
            if (!String.IsNullOrEmpty(_sourcePath))
            {
                defaultDir = Path.GetDirectoryName(_sourcePath);
            }
 
            if (!Path.IsPathRooted(defaultDir))
            {
                defaultDir = Path.Combine(Directory.GetCurrentDirectory(), defaultDir);
            }
            string[] searchPaths = { defaultDir };
            ResolveFiles(searchPaths);
        }
 
        /// <summary>
        /// Locates all specified assembly and file references by searching in the specified directories.
        /// The location of each referenced assembly and file is required for hash computation and assembly identity resolution.
        /// Any resulting errors or warnings are reported in the OutputMessages collection.
        /// </summary>
        /// <param name="searchPaths">An array of strings specify directories to search.</param>
        public void ResolveFiles(string[] searchPaths)
        {
            if (searchPaths == null)
            {
                throw new ArgumentNullException(nameof(searchPaths));
            }
 
            CollectionToArray();
            ResolveFiles_1(searchPaths);
            ResolveFiles_2(searchPaths);
        }
 
        private void ResolveFiles_1(string[] searchPaths)
        {
            if (_assemblyReferences != null)
            {
                foreach (AssemblyReference a in _assemblyReferences)
                {
                    if (!a.IsPrerequisite || a.AssemblyIdentity == null)
                    {
                        if (!ResolveAssembly(a, searchPaths))
                        {
                            if (_treatUnfoundNativeAssembliesAsPrerequisites && a.ReferenceType == AssemblyReferenceType.NativeAssembly)
                            {
                                a.IsPrerequisite = true;
                            }
                            else
                            {
                                // When we're only reading a manifest (i.e. from ResolveNativeReference task), it's
                                // very useful to report what manifest has the unresolvable reference. However, when
                                // we're generating a new manifest (i.e. from GenerateApplicationManifest task)
                                // reporting the manifest is awkward and sometimes looks like a bug.
                                // So we use the ReadOnly flag to tell the difference between the two cases...
                                if (_readOnly)
                                {
                                    OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadOnlyMode", a.ToString(), ToString());
                                }
                                else
                                {
                                    OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadWriteMode", a.ToString());
                                }
                            }
                        }
                    }
                }
            }
        }
 
        private void ResolveFiles_2(string[] searchPaths)
        {
            if (_fileReferences != null)
            {
                foreach (FileReference f in _fileReferences)
                {
                    if (!ResolveFile(f, searchPaths))
                    {
                        // When we're only reading a manifest (i.e. from ResolveNativeReference task), it's
                        // very useful to report what manifest has the unresolvable reference. However, when
                        // we're generating a new manifest (i.e. from GenerateApplicationManifest task)
                        // reporting the manifest is awkward and sometimes looks like a bug.
                        // So we use the ReadOnly flag to tell the difference between the two cases...
                        if (_readOnly)
                        {
                            OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadOnlyMode", f.ToString(), this.ToString());
                        }
                        else
                        {
                            OutputMessages.AddErrorMessage("GenerateManifest.ResolveFailedInReadWriteMode", f.ToString());
                        }
                    }
                }
            }
        }
 
        private static string ResolvePath(string path, string[] searchPaths)
        {
            if (String.IsNullOrEmpty(path))
            {
                return null;
            }
            if (Path.IsPathRooted(path))
            {
                if (FileSystems.Default.FileExists(path))
                {
                    return path;
                }
                return null;
            }
 
            if (searchPaths == null)
            {
                return null;
            }
            foreach (string searchPath in searchPaths)
            {
                if (!String.IsNullOrEmpty(searchPath))
                {
                    string resolvedPath = Path.Combine(searchPath, path);
                    resolvedPath = Path.GetFullPath(resolvedPath);
                    if (FileSystems.Default.FileExists(resolvedPath))
                    {
                        return resolvedPath;
                    }
                }
            }
            return null;
        }
 
        private void SortFiles()
        {
            CollectionToArray();
            var comparer = new ReferenceComparer();
            if (_assemblyReferences != null)
            {
                Array.Sort(_assemblyReferences, comparer);
            }
            if (_fileReferences != null)
            {
                Array.Sort(_fileReferences, comparer);
            }
        }
 
        /// <summary>
        /// Specifies the location where the manifest was loaded or saved.
        /// </summary>
        [XmlIgnore]
        public string SourcePath
        {
            get => _sourcePath;
            set => _sourcePath = value;
        }
 
        public override string ToString()
        {
            return !String.IsNullOrEmpty(_sourcePath) ? _sourcePath : AssemblyIdentity.ToString();
        }
 
        internal bool TreatUnfoundNativeAssembliesAsPrerequisites
        {
            get => _treatUnfoundNativeAssembliesAsPrerequisites;
            set => _treatUnfoundNativeAssembliesAsPrerequisites = value;
        }
 
        internal static void UpdateEntryPoint(string inputPath, string outputPath, string updatedApplicationPath, string applicationManifestPath, string targetFrameworkVersion)
        {
            var document = new XmlDocument();
            var xrSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true };
            FileStream fs = File.OpenRead(inputPath);
            using (XmlReader xr = XmlReader.Create(fs, xrSettings))
            {
                document.Load(xr);
            }
            XmlNamespaceManager nsmgr = XmlNamespaces.GetNamespaceManager(document.NameTable);
            AssemblyIdentity appManifest = AssemblyIdentity.FromManifest(applicationManifestPath);
 
            // update path to application manifest
            XmlNode codeBaseNode = null;
            foreach (string xpath in XPaths.codebasePaths)
            {
                codeBaseNode = document.SelectSingleNode(xpath, nsmgr);
                if (codeBaseNode != null)
                {
                    break;
                }
            }
 
            if (codeBaseNode == null)
            {
                throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.InvariantCulture, "XPath not found: {0}", XPaths.codebasePaths[0]));
            }
 
            codeBaseNode.Value = updatedApplicationPath;
 
            // update Public key token of application manifest
            XmlNode publicKeyTokenNode = ((XmlAttribute)codeBaseNode).OwnerElement.SelectSingleNode(XPaths.dependencyPublicKeyTokenAttribute, nsmgr);
            if (publicKeyTokenNode == null)
            {
                throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.InvariantCulture, "XPath not found: {0}", XPaths.dependencyPublicKeyTokenAttribute));
            }
 
            publicKeyTokenNode.Value = appManifest.PublicKeyToken;
 
            // update hash of application manifest
            Util.GetFileInfo(applicationManifestPath, targetFrameworkVersion, out string hash, out long size);
 
            // Hash node may not be present with optional signing
            XmlNode hashNode = ((XmlAttribute)codeBaseNode).OwnerElement.SelectSingleNode(XPaths.hashElement, nsmgr);
            if (hashNode != null)
            {
                ((XmlElement)hashNode).InnerText = hash;
            }
 
            // update file size of application manifest
            XmlAttribute sizeAttribute = ((XmlAttribute)codeBaseNode).OwnerElement.Attributes[XmlUtil.TrimPrefix(XPaths.fileSizeAttribute)];
            if (sizeAttribute == null)
            {
                throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.InvariantCulture, "XPath not found: {0}", XPaths.fileSizeAttribute));
            }
 
            sizeAttribute.Value = size.ToString(System.Globalization.CultureInfo.InvariantCulture);
 
            document.Save(outputPath);
        }
 
        private void UpdateAssemblyReference(AssemblyReference a, string targetFrameworkVersion)
        {
            if (a.IsVirtual)
            {
                return;
            }
 
            if (a.AssemblyIdentity == null)
            {
                a.AssemblyIdentity = a.ReferenceType switch
                {
                    AssemblyReferenceType.ClickOnceManifest => AssemblyIdentity.FromManifest(a.ResolvedPath),
                    AssemblyReferenceType.ManagedAssembly => AssemblyIdentity.FromManagedAssembly(a.ResolvedPath),
                    AssemblyReferenceType.NativeAssembly => AssemblyIdentity.FromNativeAssembly(a.ResolvedPath),
                    _ => AssemblyIdentity.FromFile(a.ResolvedPath),
                };
            }
 
            if (!a.IsPrerequisite)
            {
                UpdateFileReference(a, targetFrameworkVersion);
            }
 
            // If unspecified assembly type then let's figure out what it actually is...
            if (a.ReferenceType == AssemblyReferenceType.Unspecified)
            {
                // a ClickOnce deployment manifest can only refer to a ClickOnce application manifest...
                if (this is DeployManifest)
                {
                    a.ReferenceType = AssemblyReferenceType.ClickOnceManifest;
                }
                // otherwise it can only be either a managed or a native assembly, but we can only tell if we have the path...
                else if (!String.IsNullOrEmpty(a.ResolvedPath))
                {
                    if (PathUtil.IsNativeAssembly(a.ResolvedPath))
                    {
                        a.ReferenceType = AssemblyReferenceType.NativeAssembly;
                    }
                    else
                    {
                        a.ReferenceType = AssemblyReferenceType.ManagedAssembly;
                    }
                }
                // there's one other way we can tell, Type="win32" references are always native...
                else if (a.AssemblyIdentity != null && String.Equals(a.AssemblyIdentity.Type, "win32", StringComparison.OrdinalIgnoreCase))
                {
                    a.ReferenceType = AssemblyReferenceType.NativeAssembly;
                }
            }
        }
 
        private void UpdateFileReference(BaseReference f, string targetFrameworkVersion)
        {
            if (string.IsNullOrEmpty(f.ResolvedPath))
            {
                throw new FileNotFoundException(null, f.SourcePath);
            }
 
            string hash;
            long size;
            if (string.IsNullOrEmpty(targetFrameworkVersion))
            {
                Util.GetFileInfo(f.ResolvedPath, out hash, out size);
            }
            else
            {
                Util.GetFileInfo(f.ResolvedPath, targetFrameworkVersion, out hash, out size);
            }
            f.Hash = hash;
            f.Size = size;
 
            //
            // .NET >= 5 ClickOnce: If the file reference is for apphost.exe, we need to change the filename
            // in ResolvedPath to TargetPath so we don't end up publishing the file as apphost.exe.
            // If the TargetPath is not present, we will fallback to AssemblyName.
            //
            string fileName = Path.GetFileName(f.ResolvedPath);
            if (LauncherBasedDeployment &&
                fileName.Equals(Constants.AppHostExe, StringComparison.InvariantCultureIgnoreCase))
            {
                if (!string.IsNullOrEmpty(f.TargetPath))
                {
                    f.ResolvedPath = Path.Combine(Path.GetDirectoryName(f.ResolvedPath), f.TargetPath);
                }
                else if (!string.IsNullOrEmpty(AssemblyName))
                {
                    f.ResolvedPath = Path.Combine(Path.GetDirectoryName(f.ResolvedPath), AssemblyName);
                    f.TargetPath = BaseReference.GetDefaultTargetPath(f.ResolvedPath);
                }
                else
                {
                    Debug.Assert(false, "AssemblyName cannot be empty");
                    OutputMessages.AddWarningMessage("GenerateManifest.InvalidValue", "AssemblyName");
                }
            }
 
            if (string.IsNullOrEmpty(f.TargetPath))
            {
                if (!string.IsNullOrEmpty(f.SourcePath))
                {
                    f.TargetPath = BaseReference.GetDefaultTargetPath(f.SourcePath);
                }
                else
                {
                    f.TargetPath = BaseReference.GetDefaultTargetPath(Path.GetFileName(f.ResolvedPath));
                }
            }
        }
 
        /// <summary>
        /// Updates file information for each referenced assembly and file.
        /// The file information includes a hash computation and a file size for each referenced file and assembly.
        /// Also, the assembly identity is obtained for any referenced assemblies with an unspecified assembly identity.
        /// Any resulting errors or warnings are reported in the OutputMessages collection.
        /// </summary>
        public void UpdateFileInfo()
        {
            UpdateFileInfoImpl(null);
        }
 
        public void UpdateFileInfo(string targetFrameworkVersion)
        {
            UpdateFileInfoImpl(targetFrameworkVersion);
        }
 
        /// <summary>
        /// Implementation of UpdateFileInfo
        /// </summary>
        /// <param name="targetFrameworkVersion">null, if not TFV.  If no TFV, it will use sha256 signature algorithm.</param>
        private void UpdateFileInfoImpl(string targetFrameworkVersion)
        {
            if (_assemblyReferences != null)
            {
                foreach (AssemblyReference a in _assemblyReferences)
                {
                    if (!String.IsNullOrEmpty(a.ResolvedPath)) // only check resolved items...
                    {
                        try
                        {
                            UpdateAssemblyReference(a, targetFrameworkVersion);
                            if (a.AssemblyIdentity == null)
                            {
                                BadImageFormatException exception = new BadImageFormatException(null, a.ResolvedPath);
                                OutputMessages.AddErrorMessage("GenerateManifest.General", exception.Message);
                            }
                        }
                        catch (Exception e)
                        {
                            OutputMessages.AddErrorMessage("GenerateManifest.General", e.Message);
                        }
                    }
                }
            }
            if (_fileReferences != null)
            {
                foreach (FileReference f in _fileReferences)
                {
                    if (!String.IsNullOrEmpty(f.ResolvedPath)) // only check resolved items...
                    {
                        try
                        {
                            UpdateFileReference(f, targetFrameworkVersion);
                        }
                        catch (Exception e)
                        {
                            OutputMessages.AddErrorMessage("GenerateManifest.General", e.Message);
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Performs various checks to verify the validity of the manifest.
        /// Any resulting errors or warnings are reported in the OutputMessages collection.
        /// </summary>
        public virtual void Validate()
        {
            ValidateReferences();
        }
 
        private void ValidateReferences()
        {
            if (AssemblyReferences.Count <= 1)
            {
                return;
            }
 
            var identityList = new Dictionary<string, bool>();
            foreach (AssemblyReference assembly in AssemblyReferences)
            {
                if (assembly.AssemblyIdentity != null)
                {
                    // Check for two or more assemblies with the same identity...
                    string identity = assembly.AssemblyIdentity.GetFullName(AssemblyIdentity.FullNameFlags.All);
                    string key = identity.ToLowerInvariant();
                    if (!identityList.ContainsKey(key))
                    {
                        identityList.Add(key, false);
                    }
                    else if (!identityList[key])
                    {
                        OutputMessages.AddWarningMessage("GenerateManifest.DuplicateAssemblyIdentity", identity);
                        identityList[key] = true; // only warn once per identity
                    }
                }
 
                // Check that resolved assembly identity matches filename...
                if (!assembly.IsPrerequisite)
                {
                    if (assembly.AssemblyIdentity != null)
                    {
                        if (!String.Equals(
                            assembly.AssemblyIdentity.Name,
                            Path.GetFileNameWithoutExtension(assembly.TargetPath),
                            StringComparison.OrdinalIgnoreCase))
                        {
                            OutputMessages.AddWarningMessage("GenerateManifest.IdentityFileNameMismatch", assembly.ToString(), assembly.AssemblyIdentity.Name, assembly.AssemblyIdentity.Name + Path.GetExtension(assembly.TargetPath));
                        }
                    }
                }
            }
        }
 
        protected void ValidatePlatform()
        {
            foreach (AssemblyReference assembly in AssemblyReferences)
            {
                if (IsMismatchedPlatform(assembly))
                {
                    OutputMessages.AddWarningMessage("GenerateManifest.PlatformMismatch", assembly.ToString());
                }
            }
        }
 
        // Determines whether the platform of the specified assembly reference is mismatched with the applicaion's platform.
        private bool IsMismatchedPlatform(AssemblyReference assembly)
        {
            // Never flag the "Microsoft.CommonLanguageRuntime" dependency as a mismatch...
            if (assembly.IsVirtual)
            {
                return false;
            }
            // Can't tell anything if either of these are not resolved...
            if (AssemblyIdentity == null || assembly.AssemblyIdentity == null)
            {
                return false;
            }
 
            if (AssemblyIdentity.IsNeutralPlatform)
            {
                // If component is a native assembly then it is non-platform neutral by definition, so always flag as a mismatch...
                if (assembly.ReferenceType == AssemblyReferenceType.NativeAssembly)
                {
                    return true;
                }
                // Otherwise flag component as a mismatch only if it's not also platform neutral...
                return !assembly.AssemblyIdentity.IsNeutralPlatform;
            }
            else
            {
                // We want the application platform for the entry point to always match the setting for the whole application,
                // but the dependencies do not necessarily have to match...
                if (assembly != EntryPoint)
                {
                    // If application IS NOT platform neutral but the component is, then component shouldn't be flagged as a mismatch...
                    if (assembly.AssemblyIdentity.IsNeutralPlatform)
                    {
                        return false;
                    }
                }
 
                // Either we are looking at the entry point assembly or the assembly is not platform neutral.
                // We need to compare the application's platform to the component's platform,
                // if they don't match then flag component as a mismatch...
                return !String.Equals(AssemblyIdentity.ProcessorArchitecture, assembly.AssemblyIdentity.ProcessorArchitecture, StringComparison.OrdinalIgnoreCase);
            }
        }
 
        #region " XmlSerializer "
 
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlElement("AssemblyIdentity")]
        public AssemblyIdentity XmlAssemblyIdentity
        {
            get => _assemblyIdentity;
            set => _assemblyIdentity = value;
        }
 
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlArray("AssemblyReferences")]
        public AssemblyReference[] XmlAssemblyReferences
        {
            get => _assemblyReferences;
            set => _assemblyReferences = value;
        }
 
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("Description")]
        public string XmlDescription
        {
            get => _description;
            set => _description = value;
        }
 
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlArray("FileReferences")]
        public FileReference[] XmlFileReferences
        {
            get => _fileReferences;
            set => _fileReferences = value;
        }
 
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [XmlAttribute("Schema")]
        public string XmlSchema
        {
            get { return Util.Schema; }
            set { }
        }
 
        #endregion
 
 
        #region " ReferenceComparer "
 
        private class ReferenceComparer : IComparer
        {
            public int Compare(object x, object y)
            {
                if (x == null || y == null)
                {
                    Debug.Fail("Comparing null objects");
                    return 0;
                }
 
                if (x is BaseReference xRef && y is BaseReference yRef)
                {
                    if (xRef.SortName == null || yRef.SortName == null)
                    {
                        Debug.Fail("Objects do not have a SortName");
                        return 0;
                    }
 
                    return xRef.SortName.CompareTo(yRef.SortName);
                }
 
                Debug.Fail("Comparing objects that are not BaseReferences");
                return 0;
            }
        }
 
        #endregion
    }
}