src/Plugins/Get-XrmPluginSteps.ps1

<#
    .SYNOPSIS
    Retrieve plug-in steps (SDK message processing steps).

    .DESCRIPTION
    Get sdkmessageprocessingstep rows, optionally filtered by assembly, table, message and state.
    Each row also carries AssemblyName, PluginTypeName, MessageName and EntityLogicalName ("none" for a message that is not bound to a table).
    CustomOnly keeps the steps registered by customers or partners: visible steps (ishidden false, customizationlevel 1) whose plug-in assembly name does not start with "Microsoft.". The platform refuses to modify the steps registered by Microsoft.

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (Dataverse ServiceClient)

    .PARAMETER AssemblyName
    Plug-in assembly name (pluginassembly.name, without version). (Default: all)

    .PARAMETER EntityLogicalName
    Table logical name of the step filter (e.g. "account"). (Default: all)

    .PARAMETER MessageName
    SDK message name (e.g. "Create", "Update"). (Default: all)

    .PARAMETER ActiveOnly
    Keep the enabled steps only.

    .PARAMETER CustomOnly
    Keep the steps registered by customers or partners only (see description).

    .PARAMETER Columns
    Step columns to return. (Default: name, stage, mode, rank, statecode, filteringattributes, eventhandler, sdkmessageid, sdkmessagefilterid, ismanaged, customizationlevel, ishidden)

    .OUTPUTS
    PSCustomObject[]. Step rows (XrmObject) with AssemblyName, PluginTypeName, MessageName and EntityLogicalName.

    .EXAMPLE
    $steps = Get-XrmPluginSteps -XrmClient $xrmClient -EntityLogicalName "account" -MessageName "Update" -ActiveOnly -CustomOnly;
    $steps | Select-Object name, PluginTypeName, stage, rank;

    .EXAMPLE
    # Disable the steps of an assembly before a data migration
    Get-XrmPluginSteps -XrmClient $xrmClient -AssemblyName "Contoso.Plugins" -ActiveOnly | ForEach-Object { Disable-XrmPluginStep -XrmClient $xrmClient -PluginStepReference $_.Reference };

    .LINK
    https://github.com/AymericM78/PowerDataOps/blob/main/documentation/commands/Get-XrmPluginSteps.md
#>

function Get-XrmPluginSteps {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $AssemblyName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $EntityLogicalName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $MessageName,

        [Parameter(Mandatory = $false)]
        [switch]
        $ActiveOnly,

        [Parameter(Mandatory = $false)]
        [switch]
        $CustomOnly,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Columns = @("name", "stage", "mode", "rank", "statecode", "filteringattributes", "eventhandler", "sdkmessageid", "sdkmessagefilterid", "ismanaged", "customizationlevel", "ishidden")
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        $query = New-XrmQueryExpression -LogicalName "sdkmessageprocessingstep" -Columns $Columns;
        if ($ActiveOnly) {
            $query = $query | Add-XrmQueryCondition -Field "statecode" -Condition Equal -Values 0;
        }
        if ($CustomOnly) {
            $query = $query | Add-XrmQueryCondition -Field "ishidden" -Condition Equal -Values $false;
            $query = $query | Add-XrmQueryCondition -Field "customizationlevel" -Condition Equal -Values 1;
        }

        # Inner joins when filtered, outer joins otherwise (service endpoint steps have no plug-in type)
        $typeJoin = $(if ($PSBoundParameters.ContainsKey('AssemblyName')) { [Microsoft.Xrm.Sdk.Query.JoinOperator]::Inner } else { [Microsoft.Xrm.Sdk.Query.JoinOperator]::LeftOuter });
        $typeLink = $query | Add-XrmQueryLink -ToEntityName "plugintype" -FromAttributeName "eventhandler" -ToAttributeName "plugintypeid" -JoinOperator $typeJoin -Alias "pt";
        $typeLink | Add-XrmQueryLinkColumns -Columns "assemblyname", "typename" | Out-Null;
        if ($PSBoundParameters.ContainsKey('AssemblyName')) {
            $typeLink | Add-XrmQueryLinkCondition -Field "assemblyname" -Condition Equal -Values $AssemblyName | Out-Null;
        }

        $messageJoin = $(if ($PSBoundParameters.ContainsKey('MessageName')) { [Microsoft.Xrm.Sdk.Query.JoinOperator]::Inner } else { [Microsoft.Xrm.Sdk.Query.JoinOperator]::LeftOuter });
        $messageLink = $query | Add-XrmQueryLink -ToEntityName "sdkmessage" -FromAttributeName "sdkmessageid" -ToAttributeName "sdkmessageid" -JoinOperator $messageJoin -Alias "msg";
        $messageLink | Add-XrmQueryLinkColumns -Columns "name" | Out-Null;
        if ($PSBoundParameters.ContainsKey('MessageName')) {
            $messageLink | Add-XrmQueryLinkCondition -Field "name" -Condition Equal -Values $MessageName | Out-Null;
        }

        $filterJoin = $(if ($PSBoundParameters.ContainsKey('EntityLogicalName')) { [Microsoft.Xrm.Sdk.Query.JoinOperator]::Inner } else { [Microsoft.Xrm.Sdk.Query.JoinOperator]::LeftOuter });
        $filterLink = $query | Add-XrmQueryLink -ToEntityName "sdkmessagefilter" -FromAttributeName "sdkmessagefilterid" -ToAttributeName "sdkmessagefilterid" -JoinOperator $filterJoin -Alias "flt";
        $filterLink | Add-XrmQueryLinkColumns -Columns "primaryobjecttypecode" | Out-Null;
        if ($PSBoundParameters.ContainsKey('EntityLogicalName')) {
            $filterLink | Add-XrmQueryLinkCondition -Field "primaryobjecttypecode" -Condition Equal -Values $EntityLogicalName | Out-Null;
        }

        $steps = $XrmClient | Get-XrmMultipleRecords -Query $query -AsEntity -AsArray;
        foreach ($step in $steps) {
            $linked = @{};
            foreach ($alias in "pt.assemblyname", "pt.typename", "msg.name", "flt.primaryobjecttypecode") {
                $linked[$alias] = $(if ($step.Contains($alias) -and $null -ne $step[$alias]) { [string]$step[$alias].Value } else { $null });
            }
            $assemblyNameValue = $linked["pt.assemblyname"];
            if ($CustomOnly -and $assemblyNameValue -and $assemblyNameValue.StartsWith("Microsoft.", [StringComparison]::OrdinalIgnoreCase)) {
                continue;
            }

            $row = $step | ConvertTo-XrmObject;
            $row | Add-Member -MemberType NoteProperty -Name "AssemblyName" -Value $assemblyNameValue;
            $row | Add-Member -MemberType NoteProperty -Name "PluginTypeName" -Value $linked["pt.typename"];
            $row | Add-Member -MemberType NoteProperty -Name "MessageName" -Value $linked["msg.name"];
            $row | Add-Member -MemberType NoteProperty -Name "EntityLogicalName" -Value $linked["flt.primaryobjecttypecode"];
            $row;
        }
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Get-XrmPluginSteps -Alias *;

Register-ArgumentCompleter -CommandName Get-XrmPluginSteps -ParameterName "EntityLogicalName" -ScriptBlock {
    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)
    $validLogicalNames = Get-XrmEntitiesLogicalName;
    return $validLogicalNames | Where-Object { $_ -like "$wordToComplete*" };
}