File: System\Reflection\PathAssemblyResolver.cs
Web Access
Project: src\src\libraries\System.Reflection.MetadataLoadContext\src\System.Reflection.MetadataLoadContext.csproj (System.Reflection.MetadataLoadContext)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
 
namespace System.Reflection
{
    /// <summary>
    /// An assembly resolver that uses paths to every assembly that may be loaded.
    /// The file name is expected to be the same as the assembly's simple name.
    /// Multiple assemblies can exist on disk with the same name but in different directories.
    /// A single instance of PathAssemblyResolver can be used with multiple MetadataAssemblyResolver instances.
    /// </summary>
    /// <remarks>
    /// In order for an AssemblyName to match to a loaded assembly, AssemblyName.Name must be equal (casing ignored).
    /// - If AssemblyName.PublicKeyToken is specified, it must be equal.
    /// - If AssemblyName.PublicKeyToken is not specified, assemblies with no PublicKeyToken are selected over those with a PublicKeyToken.
    /// - If more than one assembly matches, the assembly with the highest Version is returned.
    /// - CultureName is ignored.
    /// </remarks>
    public class PathAssemblyResolver : MetadataAssemblyResolver
    {
        private readonly Dictionary<string, List<string>> _fileToPaths = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
 
        /// <summary>
        /// Initializes a new instance of the <see cref="System.Reflection.PathAssemblyResolver"/> class.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">Thrown when assemblyPaths is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when a path is invalid.</exception>
        public PathAssemblyResolver(IEnumerable<string> assemblyPaths)
        {
            if (assemblyPaths is null)
                throw new ArgumentNullException(nameof(assemblyPaths));
 
            foreach (string path in assemblyPaths)
            {
                if (string.IsNullOrEmpty(path))
                    throw new ArgumentException(SR.Format(SR.Arg_InvalidPath, path), nameof(assemblyPaths));
 
                string file = Path.GetFileNameWithoutExtension(path);
                if (file.Length == 0)
                    throw new ArgumentException(SR.Format(SR.Arg_InvalidPath, path), nameof(assemblyPaths));
 
                List<string>? paths;
                if (!_fileToPaths.TryGetValue(file, out paths))
                {
                    _fileToPaths.Add(file, paths = new List<string>());
                }
                paths.Add(path);
            }
        }
 
        public override Assembly? Resolve(MetadataLoadContext context, AssemblyName assemblyName)
        {
            Debug.Assert(assemblyName.Name != null);
            Assembly? candidateWithSamePkt = null;
            Assembly? candidateIgnoringPkt = null;
            if (_fileToPaths.TryGetValue(assemblyName.Name, out List<string>? paths))
            {
                ReadOnlySpan<byte> pktFromName = assemblyName.GetPublicKeyToken();
 
                foreach (string path in paths)
                {
                    Assembly assemblyFromPath = context.LoadFromAssemblyPath(path);
                    AssemblyName assemblyNameFromPath = assemblyFromPath.GetName();
                    if (assemblyName.Name.Equals(assemblyNameFromPath.Name, StringComparison.OrdinalIgnoreCase))
                    {
                        ReadOnlySpan<byte> pktFromAssembly = assemblyNameFromPath.GetPublicKeyToken();
 
                        // Find exact match on PublicKeyToken including treating no PublicKeyToken as its own entry.
                        if (pktFromName.SequenceEqual(pktFromAssembly))
                        {
                            // Pick the highest version.
                            if (candidateWithSamePkt == null || assemblyNameFromPath.Version > candidateWithSamePkt.GetName().Version)
                            {
                                candidateWithSamePkt = assemblyFromPath;
                            }
                        }
                        // If assemblyName does not specify a PublicKeyToken, or assemblyName is marked 'Retargetable',
                        // then still consider those with a PublicKeyToken and take the highest version available.
                        else if ((candidateWithSamePkt == null && pktFromName.IsEmpty) ||
                            ((assemblyName.Flags & AssemblyNameFlags.Retargetable) != 0))
                        {
                            // Pick the highest version.
                            if (candidateIgnoringPkt == null || assemblyNameFromPath.Version > candidateIgnoringPkt.GetName().Version)
                            {
                                candidateIgnoringPkt = assemblyFromPath;
                            }
                        }
                    }
                }
            }
 
            return candidateWithSamePkt ?? candidateIgnoringPkt;
        }
    }
}