Private/Resolve-DPDLLLoadOrder.ps1

function Get-DPDLLReferenceName {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter(Mandatory)]
        [string[]]$LocalAssemblyNames
    )

    $LocalAssemblyLookup = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($Name in $LocalAssemblyNames) {
        [void]$LocalAssemblyLookup.Add($Name)
    }

    $DirectoryPath = Split-Path -Path $Path -Parent
    $References = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

    # Prefer MetadataLoadContext when available to inspect references without runtime load side effects.
    $MetadataLoadContextType = [type]::GetType('System.Reflection.MetadataLoadContext, System.Reflection.MetadataLoadContext', $false)
    if ($MetadataLoadContextType) {
        try {
            $ResolverPaths = [System.Collections.Generic.List[string]]::new()
            foreach ($File in (Get-ChildItem -Path $DirectoryPath -Filter '*.dll' -File -ErrorAction SilentlyContinue)) {
                [void]$ResolverPaths.Add($File.FullName)
            }

            $TrustedPlatformAssemblies = [System.AppContext]::GetData('TRUSTED_PLATFORM_ASSEMBLIES')
            if ($TrustedPlatformAssemblies) {
                foreach ($AssemblyPath in ($TrustedPlatformAssemblies -split [System.IO.Path]::PathSeparator)) {
                    if (-not [string]::IsNullOrWhiteSpace($AssemblyPath)) {
                        [void]$ResolverPaths.Add($AssemblyPath)
                    }
                }
            }

            $PathResolver = [System.Reflection.PathAssemblyResolver]::new($ResolverPaths)
            $MetadataContext = [System.Reflection.MetadataLoadContext]::new($PathResolver)

            try {
                $Assembly = $MetadataContext.LoadFromAssemblyPath($Path)
                foreach ($Reference in $Assembly.GetReferencedAssemblies()) {
                    if ($LocalAssemblyLookup.Contains($Reference.Name)) {
                        [void]$References.Add($Reference.Name)
                    }
                }
            } finally {
                $MetadataContext.Dispose()
            }

            return @($References)
        } catch {
            Write-Verbose "MetadataLoadContext reference discovery failed for '$Path'. Falling back to ReflectionOnly APIs when available."
        }
    }

    # Windows PowerShell fallback for reference inspection on .NET Framework.
    if ([System.Reflection.Assembly].GetMethods().Name -contains 'ReflectionOnlyLoadFrom') {
        $ReflectionOnlyResolveHandler = [System.ResolveEventHandler] {
            param ($ResolveSender, $ResolveArgs)

            [void]$ResolveSender

            try {
                $RequestedName = ([System.Reflection.AssemblyName]::new($ResolveArgs.Name)).Name
                $CandidatePath = Join-Path -Path $DirectoryPath -ChildPath "$RequestedName.dll"
                if (Test-Path -Path $CandidatePath) {
                    return [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($CandidatePath)
                }
            } catch {
                return $null
            }

            return $null
        }

        try {
            [System.AppDomain]::CurrentDomain.add_ReflectionOnlyAssemblyResolve($ReflectionOnlyResolveHandler)
            $Assembly = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($Path)
            foreach ($Reference in $Assembly.GetReferencedAssemblies()) {
                if ($LocalAssemblyLookup.Contains($Reference.Name)) {
                    [void]$References.Add($Reference.Name)
                }
            }

            return @($References)
        } catch {
            Write-Verbose "ReflectionOnly reference discovery failed for '$Path'."
            return @()
        } finally {
            [System.AppDomain]::CurrentDomain.remove_ReflectionOnlyAssemblyResolve($ReflectionOnlyResolveHandler)
        }
    }

    return @()
}


function Get-DPOrdinalSortedName {
    [CmdletBinding()]
    param (
        [Parameter()]
        [AllowNull()]
        [AllowEmptyCollection()]
        [string[]]$Values = @()
    )

    if (-not $Values -or $Values.Count -eq 0) {
        return @()
    }

    $SortedValues = [System.Collections.Generic.List[string]]::new()
    foreach ($Value in $Values) {
        if (-not [string]::IsNullOrWhiteSpace($Value)) {
            [void]$SortedValues.Add($Value)
        }
    }

    $SortedValues.Sort([System.StringComparer]::OrdinalIgnoreCase)
    return @($SortedValues)
}


function Resolve-DPDLLLoadOrder {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.IO.FileInfo[]]$DLLFiles
    )

    if (-not $DLLFiles -or $DLLFiles.Count -eq 0) {
        return @()
    }

    $AssemblyNameToFile = @{}
    foreach ($DLLFile in $DLLFiles) {
        $AssemblySimpleName = $DLLFile.BaseName

        try {
            $AssemblySimpleName = [System.Reflection.AssemblyName]::GetAssemblyName($DLLFile.FullName).Name
        } catch {
            Write-Verbose "Unable to read assembly metadata from '$($DLLFile.Name)'. Falling back to base filename for graph ordering."
        }

        if (-not $AssemblyNameToFile.ContainsKey($AssemblySimpleName)) {
            $AssemblyNameToFile[$AssemblySimpleName] = $DLLFile
            continue
        }

        if ([string]::Compare(
                $DLLFile.FullName,
                $AssemblyNameToFile[$AssemblySimpleName].FullName,
                [System.StringComparison]::OrdinalIgnoreCase
            ) -lt 0) {
            $AssemblyNameToFile[$AssemblySimpleName] = $DLLFile
        }

        Write-Verbose "Duplicate assembly simple name '$AssemblySimpleName' detected. Using deterministic lexical path ordering."
    }

    $AssemblyNames = @($AssemblyNameToFile.Keys)
    $DependentsByDependency = @{}
    $InDegreeByAssembly = @{}

    foreach ($AssemblyName in $AssemblyNames) {
        $DependentsByDependency[$AssemblyName] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        $InDegreeByAssembly[$AssemblyName] = 0
    }

    foreach ($AssemblyName in $AssemblyNames) {
        $File = $AssemblyNameToFile[$AssemblyName]
        $References = @(Get-DPDLLReferenceName -Path $File.FullName -LocalAssemblyNames $AssemblyNames)

        foreach ($DependencyName in $References) {
            if (-not $AssemblyNameToFile.ContainsKey($DependencyName)) {
                continue
            }

            if ($DependentsByDependency[$DependencyName].Add($AssemblyName)) {
                $InDegreeByAssembly[$AssemblyName]++
            }
        }
    }

    $OrderedAssemblyNames = [System.Collections.Generic.List[string]]::new()
    $ProcessedAssemblyNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

    while ($OrderedAssemblyNames.Count -lt $AssemblyNames.Count) {
        $ReadyAssemblies = @(Get-DPOrdinalSortedName -Values @(
                $AssemblyNames |
                    Where-Object {
                        -not $ProcessedAssemblyNames.Contains($_) -and
                        $InDegreeByAssembly[$_] -eq 0
                    }
            ))

        if ($ReadyAssemblies.Count -eq 0) {
            break
        }

        foreach ($ReadyAssembly in $ReadyAssemblies) {
            [void]$OrderedAssemblyNames.Add($ReadyAssembly)
            [void]$ProcessedAssemblyNames.Add($ReadyAssembly)

            foreach ($DependentAssembly in (Get-DPOrdinalSortedName -Values @($DependentsByDependency[$ReadyAssembly]))) {
                $InDegreeByAssembly[$DependentAssembly]--
            }
        }
    }

    if ($OrderedAssemblyNames.Count -lt $AssemblyNames.Count) {
        $RemainingAssemblies = @(Get-DPOrdinalSortedName -Values @(
                $AssemblyNames |
                    Where-Object { -not $ProcessedAssemblyNames.Contains($_) }
            ))

        Write-Verbose "Dependency graph did not fully resolve for $($RemainingAssemblies.Count) assemblies. Appending unresolved nodes alphabetically."
        foreach ($RemainingAssembly in $RemainingAssemblies) {
            [void]$OrderedAssemblyNames.Add($RemainingAssembly)
        }
    }

    return @($OrderedAssemblyNames | ForEach-Object { $AssemblyNameToFile[$_] })
}