Copy PowerApps/Dynamics 365 Solutions with PowerShell

Copying solutions between environments is a common task in Dynamics 365 and Power Apps projects if you do your deployments via DevOps pipelines. In this post, I will explain how to accomplish this task with PowerShell.

For connecting to Dataverse via PowerShell, you can check my other post https://crmminds.com/2024/07/05/connect-to-dataverse-with-powershell/

Why Use PowerShell for Copying Solutions?

  • Automates solution management.
  • Ensures consistency across environments.
  • Avoids manual intervention during deployments.

Prerequisites

  • Install required modules.
    • Microsoft.Xrm.Data.Powershell
    • Microsoft.Xrm.Tooling.CrmConnector.PowerShell
  • Register an application in Azure Active Directory and configure it for Dataverse.

Step-by-Step Script for Copying Solutions

Here’s how you can copy a solution from one environment to another using PowerShell:

Script Overview

  1. List Existing Solutions: Use pac CLI to retrieve solution names.
  2. Check Target Solution: Verify if the target solution exists.
  3. Remove Existing Components: Clean up the target solution.
  4. Copy Components: Add components from the source to the target solution.
    • Retrieve Components: The Get-CrmRecordsByFetch command fetches components from the source solution using FetchXML.
    • Add Components: The AddSolutionComponentRequest adds these components to the target solution.
    • Handle Subcomponents: Properly manage subcomponents using the rootcomponentbehavior_Property.
# Define variables
$sourceSolutionName = ""
$targetSolutionName = ""
$clientId = ""
$clientSecret = ""
$url = ""

Write-Host "Source Solution: $sourceSolutionName"
Write-Host "Target Solution: $targetSolutionName"

# Connection string
$connString = "AuthType=ClientSecret;url=$url;ClientId=$clientId;ClientSecret=$clientSecret"
$conn = Get-CrmConnection -ConnectionString $connString

# Function: Fetch solution names
function GetSolutionNames {
    Write-Host "Fetching existing solutions..."
    $solutionList = pac solution list
    $solutionList | ForEach-Object {
        if ($_ -match "^\s*(\S+)\s+") { $matches[1] }
    }
}

# Function: Initialize target solution
function InitializeTargetSolution {
    param ($targetSolutionName)

    Write-Host "$targetSolutionName not found. Initializing a new solution..."
    $solutionPath = "$(Build.ArtifactStagingDirectory)\$targetSolutionName"
    pac solution init --publisher-name "" --publisher-prefix "" -o $solutionPath
    dotnet build $solutionPath
    pac solution import --path "$solutionPath\bin\Debug\$targetSolutionName.zip"
    Write-Host "$targetSolutionName created and imported successfully."
}

# Function: Remove components from target solution
function RemoveSolutionComponents {
    param ($conn, $targetSolutionName)

    Write-Host "Removing components from $targetSolutionName..."
    $query = @"
<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false' no-lock='true'>
  <entity name='solutioncomponent'>
    <attribute name='componenttype'/>
    <attribute name='objectid'/>
    <attribute name='solutionid'/>
    <filter>
      <condition attribute='solutionidname' operator='eq' value='$targetSolutionName' />
    </filter>
  </entity>
</fetch>
"@

    $components = Get-CrmRecordsByFetch -conn $conn -Fetch $query

    foreach ($component in $components.CrmRecords) {
        try {
            $request = [Microsoft.Crm.Sdk.Messages.RemoveSolutionComponentRequest]::new()
            $request.ComponentId = $component.objectid
            $request.componenttype = $component.componenttype_Property.Value.Value
            $request.SolutionUniqueName = $targetSolutionName

            $conn.Execute($request)
        } catch {
            Write-Host "Error removing component: $_"
        }
    }
    Write-Host "All components removed from $targetSolutionName."
}

# Function: Add components from source solution to target solution
function AddComponentsToSolution {
    param ($conn, $sourceSolutionName, $targetSolutionName)

    Write-Host "Adding components from $sourceSolutionName to $targetSolutionName..."
    $query = @"
<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false' no-lock='true'>
  <entity name='solutioncomponent'>
    <attribute name='componenttype'/>
    <attribute name='rootcomponentbehavior'/>
    <attribute name='objectid'/>
    <attribute name='solutionidname'/>
    <attribute name='solutionid'/>
    <filter>
      <condition attribute='solutionidname' operator='eq' value='$sourceSolutionName'/>
      <condition attribute='solutionidname' operator='ne' value='$targetSolutionName'/>
    </filter>
  </entity>
</fetch>
"@

    $componentsToAdd = Get-CrmRecordsByFetch -conn $conn -Fetch $query

    foreach ($component in $componentsToAdd.CrmRecords) {
        $request = [Microsoft.Crm.Sdk.Messages.AddSolutionComponentRequest]::new()
        $request.AddRequiredComponents = $false
        $request.ComponentId = $component.objectid
        $request.componenttype = $component.componenttype_Property.Value.Value
        $request.SolutionUniqueName = $targetSolutionName

        if ($component.rootcomponentbehavior_Property -and $component.rootcomponentbehavior_Property.Value) {
            $request.DoNotIncludeSubcomponents = 
                if ($component.rootcomponentbehavior_Property.Value.Value -in @(1, 2)) { $true } else { $false }
        }

        $conn.Execute($request)
    }
    Write-Host "All components added successfully."
}

# Main Execution
$solutionNames = GetSolutionNames
if ($solutionNames -notcontains $targetSolutionName) {
    InitializeTargetSolution -targetSolutionName $targetSolutionName
} else {
    RemoveSolutionComponents -conn $conn -targetSolutionName $targetSolutionName
}
AddComponentsToSolution -conn $conn -sourceSolutionName $sourceSolutionName -targetSolutionName $targetSolutionName


Key Commands Explained

  1. pac solution list: Lists solutions in the environment.
  2. pac solution init: Initializes a new solution.
  3. pac solution import: Imports the solution into Dataverse.
  4. Get-CrmRecordsByFetch: Retrieves components using FetchXML.
  5. AddSolutionComponentRequest: Adds components to the solution.

Leave a comment