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
- List Existing Solutions: Use
pacCLI to retrieve solution names. - Check Target Solution: Verify if the target solution exists.
- Remove Existing Components: Clean up the target solution.
- Copy Components: Add components from the source to the target solution.
- Retrieve Components: The
Get-CrmRecordsByFetchcommand fetches components from the source solution using FetchXML. - Add Components: The
AddSolutionComponentRequestadds these components to the target solution. - Handle Subcomponents: Properly manage subcomponents using the
rootcomponentbehavior_Property.
- Retrieve Components: The
# 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
pac solution list: Lists solutions in the environment.pac solution init: Initializes a new solution.pac solution import: Imports the solution into Dataverse.Get-CrmRecordsByFetch: Retrieves components using FetchXML.AddSolutionComponentRequest: Adds components to the solution.
Leave a comment