web
You’re offline. This is a read only version of the page.
close
Skip to main content

Announcements

News and Announcements icon
Community site session details

Community site session details

Session Id :
Power Platform Community / Forums / Power Apps / Query Holding solution...
Power Apps
Answered

Query Holding solution of each Solution object

(0) ShareShare
ReportReport
Posted on by 170
 
Hi to all,
 
Does someone know a way to query the holding solution of each of the objects in a managed solution? 
 
Expected outcome would be a list like;
 
- Object name - Holding solution name
 
Thanks,
Elowy.
 
 
 
I have the same question (0)
  • Suggested answer
    Amardeep Raj Profile Picture
    74 on at

    Hi,

    You can achieve this by using a Power Automate flow. I created one to retrieve the holding solution for each object in a managed solution. You could also implement the same logic in a Canvas App if needed.

    The output will look something like this:

    • Object Name — Holding Solution Name

    Please find the answer below along with the attached screenshot for reference.





    Thanks,
    Amar

  • Elowy GrootCRM Profile Picture
    170 on at

    Hi Amar,

    Thanks for the effort, I really appreciate it.

    I’m not sure we mean the same thing when referring to a holding solution. In my understanding, the holding (or introducing) solution is the solution that originally introduced a specific component, in cases where the component definition exists in multiple managed solutions.

    For example:

    These are the solution layers of a component (flow), and what I’m looking for is the introducing/holding solution for each layer (layer 1).

    Do you have any thoughts on this?

    Thanks,
    Elowy

  • Verified answer
    Assisted by AI
    MarkRahn Profile Picture
    1,418 Super User 2026 Season 1 on at
     
    Looking at how to tackle this problem, it appeared that there were two ways. One was writing a Flow (but I thought you needed the "Dataverse (Legacy)" connector to do that. @Amardeep Raj thanks for showing how to do that. The other was to use PowerShell. I've been looking at this for a few days to see if I could do it. With a lot of help from Copilot, I came up with the following script in PowerShell. I ran it using VS Code and installed the PowerShell extension. You need to set the $envUrl. You can also specify a specific Solution name if you want the objects from that. This will generate some CSV files that you can open in Excel and filter on. The CSV file will have columns for: Solution Id, Solution Name, Component Id, ComponentFriendlyName, Object Id, Object Name, and Holding Solution (which I think is what you are looking for).
     
    This community is supported by individuals freely devoting their time to answer questions and provide support. They do it to let you know you are not alone. This is a community.

    If someone has been able to answer your questions or solve your problem, please click Does this answer your question. This will help others who have the same question find a solution quickly via the forum search.

    If someone was able to provide you with more information that moved you closer to a solution, throw them a Like. It might make their day. 😊

    Thanks
    -Mark
    # ============================================
    # Config
    # ============================================
    $envUrl             = "https://orgxxxxxxxx.api.crm.dynamics.com"
    $targetSolutionName = "Your Solution Name here"
    $exportFolder       = "C:\Temp"
    
    if (-not (Test-Path $exportFolder)) {
        New-Item -Path $exportFolder -ItemType Directory | Out-Null
    }
    
    # ============================================
    # Ensure MSAL.PS is available
    # ============================================
    if (-not (Get-Module -ListAvailable -Name MSAL.PS)) {
        Install-Module MSAL.PS -Force
    }
    
    # ============================================
    # Authenticate
    # ============================================
    $token = Get-MsalToken `
        -ClientId "04f0c124-f2bc-4f59-8241-bf6df9866bbd" `
        -TenantId "common" `
        -Scopes "$envUrl/.default"
    
    $accessToken = $token.AccessToken
    
    $headers = @{
        "Authorization"    = "Bearer $accessToken"
        "OData-Version"    = "4.0"
        "OData-MaxVersion" = "4.0"
        "Accept"           = "application/json"
    }
    
    # ============================================
    # Component type friendly names
    # ============================================
    $ComponentTypeNames = @{
        1   = "Entity"
        2   = "Attribute"
        3   = "Relationship"
        4   = "Attribute Picklist Value"
        5   = "Attribute Lookup Value"
        6   = "View Attribute"
        7   = "Localized Label"
        8   = "Relationship Extra Condition"
        9   = "Option Set"
        10  = "Entity Relationship"
        11  = "Entity Relationship Role"
        12  = "Entity Relationship Relationships"
        13  = "Managed Property"
        14  = "Entity Key"
        16  = "Privilege"
        17  = "PrivilegeObjectTypeCode"
        18  = "Index"
        20  = "Role"
        21  = "Role Privilege"
        22  = "Display String"
        23  = "Display String Map"
        24  = "Form"
        25  = "Organization"
        26  = "Saved Query"
        29  = "Workflow"
        31  = "Report"
        32  = "Report Entity"
        33  = "Report Category"
        34  = "Report Visibility"
        35  = "Attachment"
        36  = "Email Template"
        37  = "Contract Template"
        38  = "KB Article Template"
        39  = "Mail Merge Template"
        44  = "Duplicate Rule"
        45  = "Duplicate Rule Condition"
        46  = "Entity Map"
        47  = "Attribute Map"
        48  = "Ribbon Command"
        49  = "Ribbon Context Group"
        50  = "Ribbon Customization"
        52  = "Ribbon Rule"
        53  = "Ribbon Tab To Command Map"
        55  = "Ribbon Diff"
        59  = "Saved Query Visualization"
        60  = "System Form"
        61  = "Web Resource"
        62  = "Site Map"
        63  = "Connection Role"
        64  = "Complex Control"
        65  = "Hierarchy Rule"
        66  = "Custom Control"
        68  = "Custom Control Default Config"
        70  = "Field Security Profile"
        71  = "Field Permission"
        90  = "Plugin Type"
        91  = "Plugin Assembly"
        92  = "SDK Message Processing Step"
        93  = "SDK Message Processing Step Image"
        95  = "Service Endpoint"
        150 = "Routing Rule"
        151 = "Routing Rule Item"
        152 = "SLA"
        153 = "SLA Item"
        154 = "Convert Rule"
        155 = "Convert Rule Item"
        161 = "Mobile Offline Profile"
        162 = "Mobile Offline Profile Item"
        165 = "Similarity Rule"
        166 = "Data Source Mapping"
        201 = "SDKMessage"
        202 = "SDKMessageFilter"
        203 = "SdkMessagePair"
        204 = "SdkMessageRequest"
        205 = "SdkMessageRequestField"
        206 = "SdkMessageResponse"
        207 = "SdkMessageResponseField"
        208 = "Import Map"
        210 = "WebWizard"
        300 = "Canvas App"
        371 = "Connector"
        372 = "Connector"
        380 = "Environment Variable Definition"
        381 = "Environment Variable Value"
        400 = "AI Project Type"
        401 = "AI Project"
        402 = "AI Configuration"
        430 = "Entity Analytics Configuration"
        431 = "Attribute Image Configuration"
        432 = "Entity Image Configuration"
        10020  = "Environment Variable Value"
        10021  = "Connection Role"
        10022  = "Connection Role Object Type Code"
        10049  = "Power Pages Website"
        10788 = "AI Model"
        10790 = "AI Model Component"
        10042 = "Power Pages Site Component"
        10012 = "Power Pages Content Snippet"
        10351 = "Power Pages Table Permission"
        10037 = "Power Pages Web Role"
        10041 = "Power Pages Web Page"
        10017 = "Power Pages Web Template"
        10000 = "Power Pages Website Language"
        10002 = "Power Pages Content Snippet"
        181   = "Routing Rule Set"
        10066 = "Power Pages Site Setting"
        10067 = "Power Pages Site Setting Value"
        10068 = "Power Pages Web File"
        10070 = "Power Pages Web Link"
        10940 = "Power Pages Site Component"
        270   = "SLA KPI"
        11037 = "Power Pages Scan Report"
        10427 = "Power Pages Redirect"
        80  = "Connection Reference"
        101 = "Plugin Assembly"
        10863 = "Unknown / Undocumented Component"
        10865 = "Unknown / Undocumented Component"
        183   = "Unknown / Undocumented Component"
        10072 = "Unknown / Undocumented Component"
        10003 = "Unknown / Undocumented Component"
        10043 = "Unknown / Undocumented Component"
        10092 = "Unknown / Undocumented Component"
        10034 = "Unknown / Undocumented Component"
    }
    
    # ============================================
    # Preload table list for smart unknown resolver
    # ============================================
    Write-Host "Loading Dataverse table metadata..." -ForegroundColor Cyan
    $entityList = Invoke-RestMethod `
        -Method Get `
        -Uri "$envUrl/api/data/v9.2/EntityDefinitions?`$select=LogicalName" `
        -Headers $headers
    
    $allTables = $entityList.value.logicalname
    
    # ============================================
    # Retrieve ALL solution components (paged)
    # ============================================
    $allComponents = @()
    $nextLink = "$envUrl/api/data/v9.2/solutioncomponents?" +
             "`$select=solutioncomponentid,objectid,componenttype,_solutionid_value,rootsolutioncomponentid&" +
             "`$expand=solutionid(`$select=friendlyname,uniquename)"
    
    while ($nextLink) {
        Write-Host "Fetching: $nextLink" -ForegroundColor Cyan
    
        $response = Invoke-RestMethod -Method Get -Uri $nextLink -Headers $headers
    
        $allComponents += $response.value
    
        $nextLink = $response.'@odata.nextLink'
    }
    
    Write-Host "Total components retrieved: $($allComponents.Count)" -ForegroundColor Green
    
    # Use all components for resolution
    $subset = $allComponents
    
    # ============================================
    # Parallel resolver for Canvas Apps (300), Workflows/Flows (29), and unknowns
    # ============================================
    function Resolve-ObjectNamesParallel {
        param(
            [array]$Components,
            [string]$EnvUrl,
            [hashtable]$Headers,
            [string[]]$AllTables
        )
    
        $typesToResolve = @(
            29,     # Workflows / Flows
            300,    # Canvas Apps
            24, 26, 59, 60, 61, 62,   # Forms, Views, Web Resources, Site Map
            #80,     # Connection Reference
            #90, 91, 92, 93,           # Plugin types
            380, 381                  # Environment Variables
        )
    
    
        $targets = $Components | Where-Object { $typesToResolve -contains [int]$_.componenttype }
    
        if ($targets.Count -eq 0) { return $Components }
    
        $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        $pool = [runspacefactory]::CreateRunspacePool(5, 20, $iss, $Host)
        $pool.Open()
    
        $jobs = @()
    
        foreach ($c in $targets) {
    
            $ps = [powershell]::Create()
            $ps.RunspacePool = $pool
    
            $null = $ps.AddScript({
                param($componentType, $objectId, $envUrl, $headers, $allTables)
    
                function Safe-Get {
                    param([string]$url, [string]$field)
                    try {
                        $resp = Invoke-RestMethod -Method Get -Uri $url -Headers $headers
                        return $resp.$field
                    }
                    catch { return $null }
                }
    
                function Resolve-TableName {
                    param($objectId, $envUrl, $headers, $allTables)
    
                    foreach ($table in $allTables) {
                        $url = "$envUrl/api/data/v9.2/$table($objectId)?`$select=$tableid"
                        try {
                            $null = Invoke-RestMethod -Method Get -Uri $url -Headers $headers -TimeoutSec 3
                            return $table
                        }
                        catch { }
                    }
    
                    return $null
                }
    
                function Smart-Component-Resolver {
                    param($tableName)
    
                    switch -Wildcard ($tableName) {
                        "mspp_webpage"          { return "Power Pages Web Page" }
                        "mspp_webfile"          { return "Power Pages Web File" }
                        "mspp_webrole"          { return "Power Pages Web Role" }
                        "mspp_webtemplate"      { return "Power Pages Web Template" }
                        "mspp_contentsnippet"   { return "Power Pages Content Snippet" }
                        "mspp_sitesetting"      { return "Power Pages Site Setting" }
                        "mspp_sitesettingvalue" { return "Power Pages Site Setting Value" }
                        "mspp_redirect"         { return "Power Pages Redirect" }
                        "powerpagesscanreport"  { return "Power Pages Scan Report" }
                        default                 { return "Unknown / Undocumented Component" }
                    }
                }
    
                function Resolve-UnknownType {
                    param($componentType, $objectId, $envUrl, $headers, $allTables)
    
                    $table = Resolve-TableName -objectId $objectId -envUrl $envUrl -headers $headers -allTables $allTables
                    if ($table) {
                        return Smart-Component-Resolver -tableName $table
                    }
    
                    return "Unknown / Undocumented Component"
                }
    
                switch ([int]$componentType) {
                    29 {
                        # First try workflows
                        $wfUrl = "$envUrl/api/data/v9.2/workflows($objectId)?`$select=name"
                        $name  = Safe-Get $wfUrl "name"
    
                        if (-not [string]::IsNullOrWhiteSpace($name)) {
                            return $name
                        }
    
                        # Then try flows (for Cloud Flows that might be in flow table)
                        $flowUrl = "$envUrl/api/data/v9.2/flows($objectId)?`$select=displayname"
                        $flowName = Safe-Get $flowUrl "displayname"
    
                        if (-not [string]::IsNullOrWhiteSpace($flowName)) {
                            return $flowName
                        }
    
                        return "[Workflow]"
                    }
    
                    300 {
                        $name = Safe-Get "$envUrl/api/data/v9.2/canvasapps($objectId)?`$select=displayname" "displayname"
                        if ([string]::IsNullOrWhiteSpace($name)) { return "[Canvas App]" }
                        return $name
                    }
                    24 {
                        return Safe-Get "$envUrl/api/data/v9.2/systemforms($objectId)?`$select=name" "name"
                    }
    
                    60 {
                        return Safe-Get "$envUrl/api/data/v9.2/systemforms($objectId)?`$select=name" "name"
                    }
    
                    61 {
                        return Safe-Get "$envUrl/api/data/v9.2/webresourceset($objectId)?`$select=name" "name"
                    }
    
                    80 {
                        return Safe-Get "$envUrl/api/data/v9.2/connectionreferences($objectId)?`$select=connectionreferencelogicalname" "connectionreferencelogicalname"
                    }
    
                    90 {
                        return Safe-Get "$envUrl/api/data/v9.2/plugintypes($objectId)?`$select=name" "name"
                    }
    
                    91 {
                        return Safe-Get "$envUrl/api/data/v9.2/pluginassemblies($objectId)?`$select=name" "name"
                    }
    
                    92 {
                        return Safe-Get "$envUrl/api/data/v9.2/sdkmessageprocessingsteps($objectId)?`$select=name" "name"
                    }
    
                    93 {
                        return Safe-Get "$envUrl/api/data/v9.2/sdkmessageprocessingstepimages($objectId)?`$select=name" "name"
                    }
    
                    380 {
                        return Safe-Get "$envUrl/api/data/v9.2/environmentvariabledefinitions($objectId)?`$select=schemaname" "schemaname"
                    }
    
                    381 {
                        return Safe-Get "$envUrl/api/data/v9.2/environmentvariablevalues($objectId)?`$select=value" "value"
                    }
    default { return $null }
                    #default {
                    #    return Resolve-UnknownType -componentType $componentType -objectId $objectId -envUrl $envUrl -headers $headers -allTables $allTables
                    #}
                }
    
            }).AddArgument($c.componenttype).AddArgument($c.objectid).AddArgument($EnvUrl).AddArgument($Headers).AddArgument($AllTables)
    
            $jobs += [PSCustomObject]@{
                Component  = $c
                Handle     = $ps.BeginInvoke()
                PowerShell = $ps
            }
        }
    
        # Progress bar for resolving object names
        $resolveIndex = 0
        $resolveTotal = $jobs.Count
    
        foreach ($job in $jobs) {
    
            $resolveIndex++
    
            Write-Progress `
                -Activity "Resolving Object Names" `
                -Status "Processing $resolveIndex of $resolveTotal" `
                -PercentComplete (($resolveIndex / $resolveTotal) * 100)
    
            # Get the raw runspace output
            $raw = $job.PowerShell.EndInvoke($job.Handle)
    
            # Extract the actual string value (first item)
            $value = $raw | Select-Object -First 1
    
            # Store the clean value
            $job.Component | Add-Member -NotePropertyName ObjectName -NotePropertyValue $value -Force
    
            $job.PowerShell.Dispose()
        }
    
        Write-Progress -Activity "Resolving Object Names" -Completed
    
        return $Components
    }
    
    # Run the resolver
    $subset = Resolve-ObjectNamesParallel -Components $subset -EnvUrl $envUrl -Headers $headers -AllTables $allTables
    
    # ============================================
    # Build fast lookup for root solution components
    # ============================================
    $componentLookup = @{}
    foreach ($item in $subset) {
        $componentLookup[$item.solutioncomponentid] = $item
    }
    
    # ============================================
    # Build rows (with progress)
    # ============================================
    $rowIndex = 0
    $rowTotal = $subset.Count
    
    $rowsAll = foreach ($c in $subset) {
    
        $rowIndex++
    
        Write-Progress `
            -Activity "Building Output Rows" `
            -Status "Row $rowIndex of $rowTotal (Type $($c.componenttype))" `
            -PercentComplete (($rowIndex / $rowTotal) * 100)
    
        $solutionName = $c.solutionid.friendlyname
    
        $holdingSolutionName = $null
        if ($c.rootsolutioncomponentid) {
            $root = $componentLookup[$c.rootsolutioncomponentid]
            if ($root) { $holdingSolutionName = $root.solutionid.friendlyname }
        }
    
        [PSCustomObject]@{
            SolutionId            = $c._solutionid_value
            Solution              = $solutionName
            ComponentType         = $c.componenttype
            ComponentFriendlyName = $ComponentTypeNames[[int]$c.componenttype]
            ObjectId              = $c.objectid
            ObjectName            = $c.ObjectName
            HoldingSolution       = $holdingSolutionName
        }
    }
    
    Write-Progress -Activity "Building Output Rows" -Completed
    
    $rowsTarget = $rowsAll | Where-Object { $_.Solution -eq $targetSolutionName }
    
    # ============================================
    # Unknown component types CSV
    # ============================================
    $unknownRows = $rowsAll | Where-Object {
        -not $ComponentTypeNames.ContainsKey([int]$_.ComponentType)
    }
    
    $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
    
    if ($unknownRows.Count -gt 0) {
        $unknownPath = Join-Path $exportFolder "UnknownComponentTypes_$timestamp.csv"
        $unknownRows | Export-Csv -Path $unknownPath -NoTypeInformation -Encoding UTF8
        Write-Host "Unknown component types CSV: $unknownPath"
    }
    
    # ============================================
    # Export CSVs
    # ============================================
    $allPath    = Join-Path $exportFolder "SolutionComponents_$timestamp.csv"
    $targetPath = Join-Path $exportFolder "SolutionComponents_$($targetSolutionName.Replace(' ','_'))_$timestamp.csv"
    
    $rowsAll    | Export-Csv -Path $allPath    -NoTypeInformation -Encoding UTF8
    $rowsTarget | Export-Csv -Path $targetPath -NoTypeInformation -Encoding UTF8
    
    Write-Host "All Objects CSV:        $allPath"
    Write-Host "Target solution CSV:  $targetPath"
    
     
  • Suggested answer
    Elowy GrootCRM Profile Picture
    170 on at

    @MarkRahn 

    You really helped me here, thanks!

    I made some improvements because it wasn’t showing my holding solutions correctly, and I also wanted to include all layers in the Excel output.

    Thanks a lot!

    Cheers,
    Elowy

    # ============================================
    # Config
    # ============================================
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$EnvUrl,
    
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$TargetSolutionName,
    
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ExportFolder,
    
        [bool]$VerboseErrors = $false
    )
    
    $EnvUrl = $EnvUrl.TrimEnd("/")
    
    if (-not (Test-Path $ExportFolder)) {
        New-Item -Path $ExportFolder -ItemType Directory | Out-Null
    }
    
    # ============================================
    # Authentication
    # ============================================
    if (-not (Get-Module -ListAvailable -Name MSAL.PS)) {
        Install-Module MSAL.PS -Scope CurrentUser -Force
    }
    
    Import-Module MSAL.PS -Force
    $DebugPreference = "SilentlyContinue"
    
    $token = Get-MsalToken `
        -ClientId "04f0c124-f2bc-4f59-8241-bf6df9866bbd" `
        -TenantId "organizations" `
        -Scopes "$envUrl/user_impersonation" `
        -Interactive
    
    $accessToken = $token.AccessToken
    
    $headers = @{
        "Authorization"    = "Bearer $accessToken"
        "OData-Version"    = "4.0"
        "OData-MaxVersion" = "4.0"
        "Accept"           = "application/json"
    }
    
    # ============================================
    # Component type names
    # ============================================
    $ComponentTypeNames = @{
        1     = "Entity"
        2     = "Attribute"
        10    = "Entity Relationship"
        20    = "Security Role"
        24    = "Form"
        26    = "Saved Query / View"
        29    = "Workflow / Cloud Flow"
        59    = "Saved Query Visualization / Chart"
        60    = "System Form"
        61    = "Web Resource"
        62    = "Site Map"
        80    = "Model-driven App / AppModule"
        90    = "Plugin Type"
        91    = "Plugin Assembly"
        92    = "SDK Message Processing Step"
        93    = "SDK Message Processing Step Image"
        300   = "Canvas App"
        371   = "Connector"
        372   = "Connector"
        380   = "Environment Variable Definition"
        381   = "Environment Variable Value"
        10020 = "Environment Variable Value"
        10045 = "Expense Category / Environment Specific Component"
        10048 = "Field Computation / Project Component"
        10049 = "Power Pages Website"
        10067 = "Connection Reference / Project Contract Project Price List"
        10135 = "Project / AppModule Environment Specific Component"
    }
    
    $safeGetCache = @{}
    $layerCache = @{}
    $rootSolutionCache = @{}
    
    function Escape-ODataString {
        param([string]$Value)
    
        if ($null -eq $Value) {
            return $null
        }
    
        return $Value.Replace("'", "''")
    }
    
    function Write-OptionalWarning {
        param([string]$Message)
    
        if ($verboseErrors) {
            Write-Host $Message -ForegroundColor DarkYellow
        }
    }
    
    function Invoke-DataverseGet {
        param([string]$Url)
    
        return Invoke-RestMethod -Method Get -Uri $Url -Headers $headers
    }
    
    function Get-DataverseCollection {
        param([string]$Url)
    
        $items = @()
        $nextLink = $Url
    
        while ($nextLink) {
            $response = Invoke-DataverseGet -Url $nextLink
            $items += @($response.value)
            $nextLink = $response.'@odata.nextLink'
        }
    
        return @($items)
    }
    
    function Safe-Get {
        param(
            [string]$Url,
            [string]$Field
        )
    
        $cacheKey = "$Field|$Url"
    
        if ($safeGetCache.ContainsKey($cacheKey)) {
            return $safeGetCache[$cacheKey]
        }
    
        try {
            $resp = Invoke-DataverseGet -Url $Url
            $safeGetCache[$cacheKey] = $resp.$Field
            return $safeGetCache[$cacheKey]
        }
        catch {
            Write-OptionalWarning "Could not resolve $Field from $Url. $($_.Exception.Message)"
            $safeGetCache[$cacheKey] = $null
            return $null
        }
    }
    
    # ============================================
    # Find target solution
    # ============================================
    $escapedSolutionName = Escape-ODataString $targetSolutionName
    
    $solutionUrl = "$envUrl/api/data/v9.2/solutions?" +
        "`$select=solutionid,friendlyname,uniquename&" +
        "`$filter=uniquename eq '$escapedSolutionName' or friendlyname eq '$escapedSolutionName'"
    
    $solutionResponse = Invoke-DataverseGet -Url $solutionUrl
    
    if (-not $solutionResponse.value -or $solutionResponse.value.Count -eq 0) {
        throw "Solution not found: $targetSolutionName"
    }
    
    $targetSolution   = $solutionResponse.value | Select-Object -First 1
    $targetSolutionId = $targetSolution.solutionid
    
    Write-Host "Target solution found:" -ForegroundColor Green
    Write-Host "Friendly name: $($targetSolution.friendlyname)"
    Write-Host "Unique name:   $($targetSolution.uniquename)"
    Write-Host "Solution ID:   $targetSolutionId"
    
    # ============================================
    # Retrieve target solution components only
    # ============================================
    Write-Host "Fetching target solution components..." -ForegroundColor Cyan
    
    $componentsUrl = "$envUrl/api/data/v9.2/solutioncomponents?" +
        "`$select=solutioncomponentid,objectid,componenttype,_solutionid_value,rootsolutioncomponentid&" +
        "`$expand=solutionid(`$select=friendlyname,uniquename)&" +
        "`$filter=_solutionid_value eq $targetSolutionId"
    
    $components = @(Get-DataverseCollection -Url $componentsUrl)
    
    Write-Host "Target components retrieved: $($components.Count)" -ForegroundColor Green
    
    # ============================================
    # Preload attribute metadata lookup
    # ============================================
    Write-Host "Preloading attribute metadata..." -ForegroundColor Cyan
    
    $attributeLookup = @{}
    
    try {
        $entityUrl = "$envUrl/api/data/v9.2/EntityDefinitions?`$select=LogicalName&`$expand=Attributes(`$select=MetadataId,LogicalName,SchemaName)"
        $entities = @(Get-DataverseCollection -Url $entityUrl)
    
        foreach ($entity in $entities) {
            foreach ($attr in @($entity.Attributes)) {
                if ($attr.MetadataId) {
                    $attributeLookup[$attr.MetadataId.ToString().ToLower()] = "$($entity.LogicalName).$($attr.LogicalName)"
                }
            }
        }
    
        Write-Host "Attribute metadata loaded: $($attributeLookup.Count)" -ForegroundColor Green
    }
    catch {
        Write-Host "Attribute metadata preload failed. Attribute names may remain empty." -ForegroundColor Yellow
        Write-OptionalWarning $_.Exception.Message
    }
    
    # ============================================
    # Resolve object names
    # ============================================
    function Resolve-ObjectName {
        param(
            [int]$ComponentType,
            [string]$ObjectId
        )
    
        if ([string]::IsNullOrWhiteSpace($ObjectId)) {
            return $null
        }
    
        $objectIdClean = $ObjectId.Trim("{}").ToLower()
    
        switch ($ComponentType) {
            1 {
                return Safe-Get "$envUrl/api/data/v9.2/EntityDefinitions($objectIdClean)?`$select=LogicalName" "LogicalName"
            }
    
            2 {
                if ($attributeLookup.ContainsKey($objectIdClean)) {
                    return $attributeLookup[$objectIdClean]
                }
    
                return "[Attribute]"
            }
    
            10 {
                return "[Entity Relationship]"
            }
    
            20 {
                return Safe-Get "$envUrl/api/data/v9.2/roles($objectIdClean)?`$select=name" "name"
            }
    
            24 {
                return Safe-Get "$envUrl/api/data/v9.2/systemforms($objectIdClean)?`$select=name" "name"
            }
    
            26 {
                return Safe-Get "$envUrl/api/data/v9.2/savedqueries($objectIdClean)?`$select=name" "name"
            }
    
            29 {
                $name = Safe-Get "$envUrl/api/data/v9.2/workflows($objectIdClean)?`$select=name" "name"
    
                if (-not [string]::IsNullOrWhiteSpace($name)) {
                    return $name
                }
    
                $flowName = Safe-Get "$envUrl/api/data/v9.2/flows($objectIdClean)?`$select=displayname" "displayname"
    
                if (-not [string]::IsNullOrWhiteSpace($flowName)) {
                    return $flowName
                }
    
                return "[Workflow / Cloud Flow]"
            }
    
            59 {
                return Safe-Get "$envUrl/api/data/v9.2/savedqueryvisualizations($objectIdClean)?`$select=name" "name"
            }
    
            60 {
                return Safe-Get "$envUrl/api/data/v9.2/systemforms($objectIdClean)?`$select=name" "name"
            }
    
            61 {
                return Safe-Get "$envUrl/api/data/v9.2/webresourceset($objectIdClean)?`$select=name" "name"
            }
    
            62 {
                return Safe-Get "$envUrl/api/data/v9.2/sitemaps($objectIdClean)?`$select=sitemapname" "sitemapname"
            }
    
            80 {
                return Safe-Get "$envUrl/api/data/v9.2/appmodules($objectIdClean)?`$select=name" "name"
            }
    
            90 {
                return Safe-Get "$envUrl/api/data/v9.2/plugintypes($objectIdClean)?`$select=name" "name"
            }
    
            91 {
                return Safe-Get "$envUrl/api/data/v9.2/pluginassemblies($objectIdClean)?`$select=name" "name"
            }
    
            92 {
                return Safe-Get "$envUrl/api/data/v9.2/sdkmessageprocessingsteps($objectIdClean)?`$select=name" "name"
            }
    
            93 {
                return Safe-Get "$envUrl/api/data/v9.2/sdkmessageprocessingstepimages($objectIdClean)?`$select=name" "name"
            }
    
            300 {
                return Safe-Get "$envUrl/api/data/v9.2/canvasapps($objectIdClean)?`$select=displayname" "displayname"
            }
    
            380 {
                return Safe-Get "$envUrl/api/data/v9.2/environmentvariabledefinitions($objectIdClean)?`$select=schemaname" "schemaname"
            }
    
            381 {
                return Safe-Get "$envUrl/api/data/v9.2/environmentvariablevalues($objectIdClean)?`$select=value" "value"
            }
    
            10067 {
                $name = Safe-Get "$envUrl/api/data/v9.2/connectionreferences($objectIdClean)?`$select=connectionreferencelogicalname" "connectionreferencelogicalname"
                if (-not [string]::IsNullOrWhiteSpace($name)) {
                    return $name
                }
    
                return "[Connection Reference / Project Component]"
            }
    
            default {
                return $null
            }
        }
    }
    
    # ============================================
    # Layer component-name mapping
    # ============================================
    function Get-SolutionComponentLayerName {
        param([int]$ComponentType)
    
        switch ($ComponentType) {
            1     { return "Entity" }
            2     { return "Attribute" }
            10    { return "EntityRelationship" }
            20    { return "Role" }
            24    { return "SystemForm" }
            26    { return "SavedQuery" }
            29    { return "Workflow" }
            59    { return "SavedQueryVisualization" }
            60    { return "SystemForm" }
            61    { return "WebResource" }
            62    { return "SiteMap" }
            80    { return "AppModule" }
            90    { return "PluginType" }
            91    { return "PluginAssembly" }
            92    { return "SdkMessageProcessingStep" }
            93    { return "SdkMessageProcessingStepImage" }
            300   { return "CanvasApp" }
            380   { return "EnvironmentVariableDefinition" }
            381   { return "EnvironmentVariableValue" }
            default { return $null }
        }
    }
    
    function Get-SolutionComponentLayerNames {
        param([int]$ComponentType)
    
        switch ($ComponentType) {
            10045 { return @("ExpenseCategory", "msdyn_expensecategory") }
            10048 { return @("FieldComputation", "msdyn_fieldcomputation") }
            10067 { return @("connectionreference", "ConnectionReference", "ProjectContractProjectPriceList", "msdyn_projectcontractprojectpricelist") }
            10135 { return @("AppModule", "appmodule", "msdyn_appmodule") }
    
            default {
                $single = Get-SolutionComponentLayerName -ComponentType $ComponentType
                if ($single) {
                    return @($single)
                }
    
                return @()
            }
        }
    }
    
    # ============================================
    # Get solution layers
    # ============================================
    function Get-SolutionLayers {
        param(
            [int]$ComponentType,
            [string]$ObjectId
        )
    
        if ([string]::IsNullOrWhiteSpace($ObjectId)) {
            return @()
        }
    
        $objectIdClean = $ObjectId.Trim("{}").ToLower()
        $cacheKey = "$ComponentType|$objectIdClean"
    
        if ($layerCache.ContainsKey($cacheKey)) {
            return @($layerCache[$cacheKey])
        }
    
        $componentLayerNames = Get-SolutionComponentLayerNames -ComponentType $ComponentType
    
        foreach ($componentLayerName in $componentLayerNames) {
            $layerUrl = "$envUrl/api/data/v9.0/msdyn_componentlayers?" +
                "`$filter=(msdyn_componentid eq '$objectIdClean' and msdyn_solutioncomponentname eq '$componentLayerName')"
    
            try {
                $layerResponse = Invoke-DataverseGet -Url $layerUrl
    
                if ($layerResponse.value -and $layerResponse.value.Count -gt 0) {
                    $layers = @($layerResponse.value | Sort-Object msdyn_order -Descending)
                    $layerCache[$cacheKey] = $layers
                    return @($layers)
                }
            }
            catch {
                Write-OptionalWarning "Layer lookup failed for $componentLayerName / $objectIdClean. $($_.Exception.Message)"
            }
        }
    
        $layerCache[$cacheKey] = @()
        return @()
    }
    
    # ============================================
    # Determine holding/base solution from layers
    # ============================================
    function Get-HoldingSolutionFromLayers {
        param(
            [array]$Layers,
            [string]$CurrentSolutionUniqueName,
            [string]$CurrentSolutionFriendlyName
        )
    
        if (-not $Layers -or $Layers.Count -eq 0) {
            return $null
        }
    
        $managedLayers = @($Layers | Where-Object {
            $_.msdyn_solutionname -ne "Active"
        })
    
        if (-not $managedLayers -or $managedLayers.Count -eq 0) {
            return $null
        }
    
        $explicitHolding = $managedLayers | Where-Object {
            $_.msdyn_solutionname -like "*_Upgrade*" -or
            $_.msdyn_solutionname -like "*Holding*" -or
            $_.msdyn_solutionname -like "*holding*"
        } | Sort-Object msdyn_order | Select-Object -First 1
    
        if ($explicitHolding) {
            return $explicitHolding
        }
    
        $baseLayer = $managedLayers | Where-Object {
            $_.msdyn_solutionname -ne $CurrentSolutionUniqueName -and
            $_.msdyn_solutionname -ne $CurrentSolutionFriendlyName
        } | Sort-Object msdyn_order | Select-Object -First 1
    
        if ($baseLayer) {
            return $baseLayer
        }
    
        return $null
    }
    
    function Get-RootSolutionInfo {
        param(
            [string]$RootSolutionComponentId,
            [hashtable]$ComponentLookup
        )
    
        if ([string]::IsNullOrWhiteSpace($RootSolutionComponentId)) {
            return $null
        }
    
        $cacheKey = $RootSolutionComponentId.ToLower()
    
        if ($rootSolutionCache.ContainsKey($cacheKey)) {
            return $rootSolutionCache[$cacheKey]
        }
    
        if ($ComponentLookup.ContainsKey($RootSolutionComponentId)) {
            $info = [PSCustomObject]@{
                FriendlyName = $ComponentLookup[$RootSolutionComponentId].solutionid.friendlyname
                UniqueName   = $ComponentLookup[$RootSolutionComponentId].solutionid.uniquename
            }
    
            $rootSolutionCache[$cacheKey] = $info
            return $info
        }
    
        try {
            $rootUrl = "$envUrl/api/data/v9.2/solutioncomponents($RootSolutionComponentId)?`$select=solutioncomponentid&`$expand=solutionid(`$select=friendlyname,uniquename)"
            $root = Invoke-DataverseGet -Url $rootUrl
    
            $info = [PSCustomObject]@{
                FriendlyName = $root.solutionid.friendlyname
                UniqueName   = $root.solutionid.uniquename
            }
    
            $rootSolutionCache[$cacheKey] = $info
            return $info
        }
        catch {
            Write-OptionalWarning "Root solution lookup failed for $RootSolutionComponentId. $($_.Exception.Message)"
            $rootSolutionCache[$cacheKey] = $null
            return $null
        }
    }
    
    # ============================================
    # Build lookup
    # ============================================
    $componentLookup = @{}
    
    foreach ($c in $components) {
        $componentLookup[$c.solutioncomponentid] = $c
    }
    
    # ============================================
    # Build output rows
    # ============================================
    $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
    
    $rows = @()
    $rowIndex = 0
    $rowTotal = $components.Count
    
    foreach ($c in $components) {
        $rowIndex++
    
        Write-Progress `
            -Activity "Building target solution output" `
            -Status "Component $rowIndex of $rowTotal" `
            -PercentComplete (($rowIndex / $rowTotal) * 100)
    
        $componentType = [int]$c.componenttype
        $objectIdClean = if ($c.objectid) { $c.objectid.Trim("{}").ToLower() } else { $null }
    
        $componentFriendlyName = $ComponentTypeNames[$componentType]
    
        if ([string]::IsNullOrWhiteSpace($componentFriendlyName)) {
            $componentFriendlyName = "Unknown / Undocumented Component"
        }
    
        $objectName = Resolve-ObjectName `
            -ComponentType $componentType `
            -ObjectId $c.objectid
    
        $layers = @(Get-SolutionLayers `
            -ComponentType $componentType `
            -ObjectId $c.objectid)
    
        $layerNames = ($layers | ForEach-Object {
            "$($_.msdyn_order):$($_.msdyn_solutionname):$($_.msdyn_solutioncomponentname)"
        }) -join " | "
    
        $layerComponentName = if ($layers.Count -gt 0) {
            $layers[0].msdyn_solutioncomponentname
        }
        else {
            $null
        }
    
        $holdingLayer = Get-HoldingSolutionFromLayers `
            -Layers $layers `
            -CurrentSolutionUniqueName $c.solutionid.uniquename `
            -CurrentSolutionFriendlyName $c.solutionid.friendlyname
    
        $holdingSolutionName       = $null
        $holdingSolutionUniqueName = $null
        $holdingLayerId            = $null
        $holdingSource             = $null
        $holdingOrder              = $null
    
        if ($holdingLayer) {
            $holdingSolutionName       = $holdingLayer.msdyn_solutionname
            $holdingSolutionUniqueName = $holdingLayer.msdyn_solutionname
            $holdingLayerId            = $holdingLayer.msdyn_componentlayerid
            $holdingOrder              = $holdingLayer.msdyn_order
            $holdingSource             = "msdyn_componentlayers"
        }
    
        if ([string]::IsNullOrWhiteSpace($holdingSolutionName)) {
            $rootSolutionInfo = Get-RootSolutionInfo `
                -RootSolutionComponentId $c.rootsolutioncomponentid `
                -ComponentLookup $componentLookup
    
            if ($rootSolutionInfo) {
                $holdingSolutionName       = $rootSolutionInfo.FriendlyName
                $holdingSolutionUniqueName = $rootSolutionInfo.UniqueName
                $holdingSource             = "rootsolutioncomponentid"
            }
        }
    
        $rows += [PSCustomObject]@{
            SolutionId                = $c._solutionid_value
            Solution                  = $c.solutionid.friendlyname
            SolutionUniqueName        = $c.solutionid.uniquename
            ComponentType             = $componentType
            ComponentFriendlyName     = $componentFriendlyName
            ObjectId                  = $c.objectid
            ObjectName                = $objectName
    
            HoldingSolution           = $holdingSolutionName
            HoldingSolutionUniqueName = $holdingSolutionUniqueName
            HoldingLayerId            = $holdingLayerId
            HoldingSolutionOrder      = $holdingOrder
            HoldingSolutionSource     = $holdingSource
    
            LayerCount                = $layers.Count
            LayerComponentName        = $layerComponentName
            LayerQueryObjectId        = $objectIdClean
            RootSolutionComponent     = $c.rootsolutioncomponentid
            SolutionLayers            = $layerNames
        }
    }
    
    Write-Progress -Activity "Building target solution output" -Completed
    
    # ============================================
    # Export target solution components
    # ============================================
    $targetSafeName = $targetSolution.uniquename.Replace(" ", "_")
    $targetPath = Join-Path $exportFolder "SolutionComponents_$($targetSafeName)_$timestamp.csv"
    
    $rows | Export-Csv -Path $targetPath -NoTypeInformation -Encoding UTF8
    
    Write-Host "Target solution CSV: $targetPath" -ForegroundColor Green
    
    # ============================================
    # Export unknown or unresolved rows
    # ============================================
    $unknownRows = @($rows | Where-Object {
        -not $ComponentTypeNames.ContainsKey([int]$_.ComponentType) -or
        [string]::IsNullOrWhiteSpace($_.HoldingSolution)
    })
    
    if ($unknownRows.Count -gt 0) {
        $unknownPath = Join-Path $exportFolder "UnknownOrUnresolvedComponentTypes_$timestamp.csv"
        $unknownRows | Export-Csv -Path $unknownPath -NoTypeInformation -Encoding UTF8
        Write-Host "Unknown/unresolved component types CSV: $unknownPath" -ForegroundColor Yellow
    }
    else {
        Write-Host "No unknown or unresolved component types found." -ForegroundColor Green
    }
    
     
  • Verified answer
    MarkRahn Profile Picture
    1,418 Super User 2026 Season 1 on at
     
    I’m glad to see everything come together, and you did a nice job refining the final script.

    Since the solution you arrived at is built directly on the PowerShell foundation I provided — which took several days of work and testing — would you mind marking my earlier reply as an answer?

    It helps others find the correct starting point, and it also ensures I maintain my Super User status so I can continue contributing at this level.

    Thanks for your understanding, and I’m glad we were able to get you to a working solution.

    Thanks -Mark

Under review

Thank you for your reply! To ensure a great experience for everyone, your content is awaiting approval by our Community Managers. Please check back later.

Helpful resources

Quick Links

Season of Sharing Community Challenge Launch!

Jump in, show your community spirit, and win prizes!

Kudos to our 2025 Community Spotlight Honorees

Expanding mentorship, skilling, and AI innovation

Congratulations to the May Top 10 Community Leaders!

These are the community rock stars!

Leaderboard > Power Apps

#1
WarrenBelz Profile Picture

WarrenBelz 357 Most Valuable Professional

#2
Valantis Profile Picture

Valantis 326

#3
11manish Profile Picture

11manish 284

Last 30 days Overall leaderboard