Source/Public/Sort-ObjectGraph.ps1

<#
.SYNOPSIS
    Sort an object graph
 
.DESCRIPTION
    Recursively sorts a object graph.
 
.PARAMETER InputObject
    The input object that will be recursively sorted.
 
    > [!NOTE]
    > Multiple input object might be provided via the pipeline.
    > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline.
    > To avoid a list of (root) objects to unroll, use the **comma operator**:
 
        ,$InputObject | Sort-Object.
 
.PARAMETER PrimaryKey
    Any primary key defined by the [-PrimaryKey] parameter will be put on top of [-InputObject]
    independent of the (descending) sort order.
 
    It is allowed to supply multiple primary keys.
 
.PARAMETER MatchCase
    (Alias `-CaseSensitive`) Indicates that the sort is case-sensitive. By default, sorts aren't case-sensitive.
 
.PARAMETER Descending
    Indicates that Sort-Object sorts the objects in descending order. The default is ascending order.
 
    > [!NOTE]
    > Primary keys (see: [-PrimaryKey]) will always put on top.
 
.PARAMETER MaxDepth
    The maximal depth to recursively compare each embedded property (default: 10).
#>


function ConvertTo-SortedObjectGraph {
    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs', '')]
    [CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Sort-ObjectGraph.md')][OutputType([Object[]])] param(

        [Parameter(Mandatory = $true, ValueFromPipeLine = $True)]
        $InputObject,

        [Alias('By')][String[]]$PrimaryKey,

        [Alias('CaseSensitive')]
        [Switch]$MatchCase,

        [Switch]$Descending,

        [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth
    )
    begin {
        $ObjectComparison = [ObjectComparison]0
        if ($MatchCase)  { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchCase'}
        if ($Descending) { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'Descending'}
        # As the child nodes are sorted first, we just do a side-by-side node compare:
        $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder'

        $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison }
        $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison }

        function SortRecurse([PSCollectionNode]$Node, [PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) {
            $NodeList = $Node.GetNodeList()
            for ($i = 0; $i -lt $NodeList.Count; $i++) {
                if ($NodeList[$i] -is [PSCollectionNode]) {
                    $NodeList[$i] = SortRecurse $NodeList[$i] -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer
                }
            }
            if ($Node -is [PSListNode]) {
                $NodeList.Sort($PSListNodeComparer)
                if ($NodeList.Count) { $Node.Value = @($NodeList.Value) } else { $Node.Value =  @() }
            }
            else { # if ($Node -is [PSMapNode])
                $NodeList.Sort($PSMapNodeComparer)
                $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal)
                foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException)
                if ($Node -is [PSObjectNode]) { $Node.Value = [PSCustomObject]$Properties } else { $Node.Value = $Properties }
            }
            $Node
        }
    }

    process {
        $Node = [PSNode]::ParseInput($InputObject, $MaxDepth)
        if ($Node -is [PSCollectionNode]) {
            $Node = SortRecurse $Node -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer
        }
        $Node.Value
    }
}

Set-Alias -Name 'Sort-ObjectGraph' -Value 'ConvertTo-SortedObjectGraph' -Scope Global