Private/AssemblyReflection.ps1

function Invoke-WithAssemblyResolution {
    <#
    .SYNOPSIS
        Executes a script block with assembly resolution handling.
    .DESCRIPTION
        Loads a .NET assembly with proper dependency resolution. Dependencies
        are resolved from the same directory as the main DLL.
    .PARAMETER DllPath
        Path to the DLL to load.
    .PARAMETER ScriptBlock
        Script block to execute with the loaded assembly. Receives the assembly
        as a parameter.
    .OUTPUTS
        The result of the script block execution.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$DllPath,

        [Parameter(Mandatory = $true)]
        [scriptblock]$ScriptBlock
    )

    if (-not (Test-Path $DllPath)) {
        throw "DLL not found: $DllPath"
    }

    $dllFullPath = (Resolve-Path $DllPath).Path
    $dllDir = Split-Path $dllFullPath -Parent

    # Create handler to resolve dependencies from the same directory
    $resolveHandler = [System.ResolveEventHandler]{
        param($s, $e)
        $assemblyName = (New-Object System.Reflection.AssemblyName($e.Name)).Name
        $localDllPath = Join-Path $dllDir "$assemblyName.dll"
        if (Test-Path $localDllPath) {
            return [System.Reflection.Assembly]::LoadFrom($localDllPath)
        }
        return $null
    }

    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($resolveHandler)

    try {
        $assembly = [System.Reflection.Assembly]::LoadFrom($dllFullPath)
        return & $ScriptBlock $assembly
    }
    finally {
        [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($resolveHandler)
    }
}

function Get-PluginRegistrationsFromAssembly {
    <#
    .SYNOPSIS
        Extracts plugin registrations from a compiled assembly using reflection.
    .PARAMETER DllPath
        Path to the compiled plugin DLL.
    .OUTPUTS
        Array of plugin registration objects.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$DllPath
    )

    return Invoke-WithAssemblyResolution -DllPath $DllPath -ScriptBlock {
        param($assembly)
        $plugins = @()

        foreach ($type in $assembly.GetExportedTypes()) {
            if ($type.IsAbstract -or $type.IsInterface) {
                continue
            }

            $stepAttributes = $type.GetCustomAttributes($true) | Where-Object {
                $_.GetType().Name -eq "PluginStepAttribute"
            }

            if ($stepAttributes.Count -eq 0) {
                continue
            }

            $imageAttributes = $type.GetCustomAttributes($true) | Where-Object {
                $_.GetType().Name -eq "PluginImageAttribute"
            }

            $steps = @()
            foreach ($stepAttr in $stepAttributes) {
                $stepId = $stepAttr.StepId
                $stepName = if ($stepAttr.Name) {
                    $stepAttr.Name
                } else {
                    "$($type.FullName): $($stepAttr.Message) of $($stepAttr.EntityLogicalName)"
                }

                $stageValue = [int]$stepAttr.Stage
                $modeValue = [int]$stepAttr.Mode
                $stageName = $script:PluginStageMap[$stageValue]
                $modeName = $script:PluginModeMap[$modeValue]

                $stepImages = @()
                foreach ($imageAttr in $imageAttributes) {
                    $shouldInclude = if ($stepId -and $imageAttr.StepId) {
                        $stepId -eq $imageAttr.StepId
                    } elseif (-not $stepId -and -not $imageAttr.StepId) {
                        $true
                    } elseif ($stepAttributes.Count -eq 1 -and -not $imageAttr.StepId) {
                        $true
                    } else {
                        $false
                    }

                    if ($shouldInclude) {
                        $imageTypeValue = [int]$imageAttr.ImageType
                        $imageTypeName = $script:PluginImageTypeMap[$imageTypeValue]

                        $stepImages += [PSCustomObject]@{
                            name = $imageAttr.Name
                            imageType = $imageTypeName
                            attributes = $imageAttr.Attributes
                            entityAlias = if ($imageAttr.EntityAlias) { $imageAttr.EntityAlias } else { $imageAttr.Name }
                        }
                    }
                }

                $steps += [PSCustomObject]@{
                    name = $stepName
                    message = $stepAttr.Message
                    entity = $stepAttr.EntityLogicalName
                    secondaryEntity = $stepAttr.SecondaryEntityLogicalName
                    stage = $stageName
                    mode = $modeName
                    executionOrder = $stepAttr.ExecutionOrder
                    filteringAttributes = $stepAttr.FilteringAttributes
                    configuration = $stepAttr.UnsecureConfiguration
                    stepId = $stepId
                    images = $stepImages
                }
            }

            $plugins += [PSCustomObject]@{
                typeName = $type.FullName
                steps = $steps
            }
        }

        return $plugins
    }
}

function Get-AllPluginTypeNames {
    <#
    .SYNOPSIS
        Gets all plugin and workflow activity type names from a compiled assembly.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$DllPath
    )

    return Invoke-WithAssemblyResolution -DllPath $DllPath -ScriptBlock {
        param($assembly)
        $typeNames = @()

        foreach ($type in $assembly.GetExportedTypes()) {
            if ($type.IsAbstract -or $type.IsInterface) {
                continue
            }

            $isPlugin = $false

            foreach ($iface in $type.GetInterfaces()) {
                if ($iface.Name -eq "IPlugin" -or $iface.FullName -eq "Microsoft.Xrm.Sdk.IPlugin") {
                    $isPlugin = $true
                    break
                }
            }

            if (-not $isPlugin) {
                $baseType = $type.BaseType
                while ($null -ne $baseType) {
                    if ($baseType.Name -eq "CodeActivity" -or $baseType.FullName -like "*CodeActivity") {
                        $isPlugin = $true
                        break
                    }
                    $baseType = $baseType.BaseType
                }
            }

            if ($isPlugin) {
                $typeNames += $type.FullName
            }
        }

        return $typeNames
    }
}

function Get-PluginProjects {
    <#
    .SYNOPSIS
        Discovers plugin projects in a repository.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$RepositoryRoot
    )

    $projects = @()

    $classicPath = Join-Path $RepositoryRoot "src/Plugins"
    if (Test-Path $classicPath) {
        Get-ChildItem -Path $classicPath -Filter "*.csproj" -Recurse | ForEach-Object {
            $projectDir = $_.DirectoryName
            $projectName = $_.BaseName

            $dllPath = Join-Path $projectDir "bin/Release/net462/$projectName.dll"
            $debugDllPath = Join-Path $projectDir "bin/Debug/net462/$projectName.dll"

            $actualDllPath = if (Test-Path $dllPath) { $dllPath }
                             elseif (Test-Path $debugDllPath) { $debugDllPath }
                             else { $null }

            $projects += [PSCustomObject]@{
                Name = $projectName
                ProjectPath = $_.FullName
                ProjectDir = $projectDir
                Type = "Assembly"
                DllPath = $actualDllPath
                RelativeDllPath = if ($actualDllPath) {
                    (Resolve-Path -Path $actualDllPath -Relative -ErrorAction SilentlyContinue) -replace '\\','/'
                } else { $null }
            }
        }
    }

    $packagePath = Join-Path $RepositoryRoot "src/PluginPackages"
    if (Test-Path $packagePath) {
        Get-ChildItem -Path $packagePath -Filter "*.csproj" -Recurse | ForEach-Object {
            $projectDir = $_.DirectoryName
            $projectName = $_.BaseName

            $dllPath = Join-Path $projectDir "bin/Release/net462/$projectName.dll"
            $debugDllPath = Join-Path $projectDir "bin/Debug/net462/$projectName.dll"

            $actualDllPath = if (Test-Path $dllPath) { $dllPath }
                             elseif (Test-Path $debugDllPath) { $debugDllPath }
                             else { $null }

            $nupkgPattern = Join-Path $projectDir "bin/Release/*.nupkg"
            $nupkgFile = Get-ChildItem -Path $nupkgPattern -ErrorAction SilentlyContinue |
                         Sort-Object LastWriteTime -Descending |
                         Select-Object -First 1

            $projects += [PSCustomObject]@{
                Name = $projectName
                ProjectPath = $_.FullName
                ProjectDir = $projectDir
                Type = "Nuget"
                DllPath = $actualDllPath
                NupkgPath = $nupkgFile.FullName
                RelativeDllPath = if ($actualDllPath) {
                    (Resolve-Path -Path $actualDllPath -Relative -ErrorAction SilentlyContinue) -replace '\\','/'
                } else { $null }
                RelativeNupkgPath = if ($nupkgFile) {
                    (Resolve-Path -Path $nupkgFile.FullName -Relative -ErrorAction SilentlyContinue) -replace '\\','/'
                } else { $null }
            }
        }
    }

    return $projects
}