Netscoot.Shared/Dotnet/Dotnet.ps1

function New-DotnetReferenceItems {
    # Build the standard reconciliation items for a managed project move: solution membership,
    # external consumers, and the project's own references - each a detach+reattach pair so the
    # plan engine can confirm/skip per line. The dotnet ref-op scriptblocks live here, once.
    [CmdletBinding()]
    param(
        [object[]]$Solutions = @(),   # objects with .FullName + .Name
        [string[]]$Consumers = @(),   # consumer project paths
        [object[]]$OwnRefs = @(),     # objects with .FullPath
        [Parameter(Mandatory)][string]$OldProj,
        [Parameter(Mandatory)][string]$NewProj,
        [string]$Label = ''
    )
    # Per-edge scriptblocks (the un-batched fallback path: Invoke-MovePlan runs these one at a
    # time when an item carries no batch metadata).
    $slnRemove = { param($Sln, $Proj) Invoke-Dotnet sln $Sln remove $Proj }
    $slnAdd = { param($Sln, $Proj) Invoke-Dotnet sln $Sln add $Proj }
    $refRemove = { param($Consumer, $Proj) Invoke-Dotnet remove $Consumer reference $Proj }
    $refAdd = { param($Consumer, $Proj) Invoke-Dotnet add $Consumer reference $Proj }
    $ownRemove = { param($Proj, $Target) Invoke-Dotnet remove $Proj reference $Target }
    $ownAdd = { param($Proj, $Target) Invoke-Dotnet add $Proj reference $Target }
    $sfx = if ($Label) { " ($Label)" } else { '' }

    # Batch metadata lets Invoke-MovePlan collapse every edge that shares one dotnet target file
    # into a single spawn (the dotnet CLI takes multiple projects per invocation). Each phase entry
    # is { Key; Prefix; Item }: Key groups co-spawned edges, Prefix is the fixed leading args, and
    # Item is the one variable project token appended (one per edge) to the shared command line.
    # Key embeds the verb so a remove and an add to the same file never merge.
    $items = @()
    foreach ($sln in $Solutions) {
        $items += New-MoveItem -Description "solution membership: $($sln.Name)$sfx" `
            -Detach $slnRemove -DetachArgs @($sln.FullName, $OldProj) `
            -Reattach $slnAdd -ReattachArgs @($sln.FullName, $NewProj) `
            -DetachBatch @{ Key = "sln|remove|$($sln.FullName)"; Prefix = @('sln', $sln.FullName, 'remove'); Item = $OldProj } `
            -ReattachBatch @{ Key = "sln|add|$($sln.FullName)"; Prefix = @('sln', $sln.FullName, 'add'); Item = $NewProj }
    }
    foreach ($c in $Consumers) {
        $items += New-MoveItem -Description "consumer reference: $(Split-Path -Leaf $c)$sfx" `
            -Detach $refRemove -DetachArgs @($c, $OldProj) `
            -Reattach $refAdd -ReattachArgs @($c, $NewProj) `
            -DetachBatch @{ Key = "ref|remove|$c"; Prefix = @('remove', $c, 'reference'); Item = $OldProj } `
            -ReattachBatch @{ Key = "ref|add|$c"; Prefix = @('add', $c, 'reference'); Item = $NewProj }
    }
    foreach ($r in $OwnRefs) {
        $items += New-MoveItem -Description "own reference: $(Split-Path -Leaf $r.FullPath)$sfx" `
            -Detach $ownRemove -DetachArgs @($OldProj, $r.FullPath) `
            -Reattach $ownAdd -ReattachArgs @($NewProj, $r.FullPath) `
            -DetachBatch @{ Key = "own|remove|$OldProj"; Prefix = @('remove', $OldProj, 'reference'); Item = $r.FullPath } `
            -ReattachBatch @{ Key = "own|add|$NewProj"; Prefix = @('add', $NewProj, 'reference'); Item = $r.FullPath }
    }
    return $items
}

function Invoke-DotnetRead {
    # Read-only dotnet call: returns stdout lines, swallows stderr, never throws on it.
    # Windows PowerShell 5.1 turns native stderr into a terminating error when
    # $ErrorActionPreference is Stop; force Continue around the call so it does not.
    [CmdletBinding()]
    param([Parameter(Mandatory, ValueFromRemainingArguments)][string[]]$Arguments)
    $prev = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    try { return (& dotnet @Arguments 2>$null) }
    finally { $ErrorActionPreference = $prev }
}

function Invoke-Dotnet {
    # Mutating dotnet call: runs, then throws on non-zero exit. Same 5.1 stderr guard.
    [CmdletBinding()]
    param([Parameter(Mandatory, ValueFromRemainingArguments)][string[]]$Arguments)
    Write-Verbose "dotnet $($Arguments -join ' ')"
    $prev = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    try { & dotnet @Arguments 2>&1 | Write-Verbose }
    finally { $ErrorActionPreference = $prev }
    if ($LASTEXITCODE -ne 0) {
        throw "dotnet $($Arguments -join ' ') failed with exit code $LASTEXITCODE"
    }
}