M365DSCTools.psm1

#Region './Classes/PSNode.ps1' 0
<#
.SYNOPSIS
    PowerShell Object Node Class

.DESCRIPTION
    This class provides general properties and method to recursively
    iterate through to PowerShell Object Graph nodes.

## Usage

To create a root node, you might simply construct a `[PSNode]` instance from the root object:

```PowerShell
[PSNode]$MyObject
```

To ensure that the recursive properties and method are being up to date with the object depth,
it is imperative that any child node is created from the parent node when passed to any recursive
function using the `GetItemNodes()` or `GetItemNode(<index/key>)` methods:

```PowerShell
function MyRecursiveFunction([PSNode]$Node) {
    Write-Host $Node.GetPathName() '=' $Node.Value
    foreach ($ChildNode in $Node.GetItemNodes()) {
        MyRecursiveFunction($ChildNode)
    }
}
MyRecursiveFunction($MyObject)
```

## properties

### `MaxDepth`

Defines the class wide (static) maximum node depth of the object.
If the maximum depth has been reached, a error will be thrown.

### `Depth`

The current depth of the node.

### `Index`

The item node index relative to parent list or array. (ReadOnly, do not set)

### `Key`

The item node key or property relative to parent dictionary or PowerShell object.

#>


[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Name', Justification = 'False positive')]
param()
enum Construction { Undefined; Scalar; List; Dictionary; Object }

Class PSNode {
    static [int]$DefaultMaxDepth = 10
    [Int]$MaxDepth = [PSNode]::DefaultMaxDepth
    $Key                                    # The dictionary key or property name of the node
    $Index                                  # This index of $this item
    [Int]$Depth
    [PSNode]$Parent
    hidden $Path
    hidden $PathName

    $Value
    [Type]$Type
    [Construction]$Construction
    [Construction]$Structure

    PSNode($Object) {
        if ($Object -is [PSNode]) { $this.Value = $Object.Value } else { $this.Value = $Object }
        if ($Null -ne $Object)    { $this.Type = $Object.GetType() }
        $this.Construction =
            if ($Object -is [Management.Automation.PSCustomObject]) { 'Object' }
        elseif ($Object -is [ComponentModel.Component])             { 'Object' }
        elseif ($Object -is [Collections.IDictionary])              { 'Dictionary' }
        elseif ($Object -is [Collections.ICollection])              { 'List' }
        else                                                        { 'Scalar' }
        $this.Structure = if ($this.Construction -le 'Dictionary') { $this.Construction } else { 'Dictionary' }
    }

    [Array]GetPath() {
        if ($Null -eq $this.Path) {
            if     ($Null -ne $this.Index) { $this.Path = $this.Parent.GetPath() + $this.Index }
            elseif ($Null -ne $this.Key)   { $this.Path = $this.Parent.GetPath() + $this.Key }
            else                           { $this.Path = @() }
        }
        return $this.Path
    }

    [String]GetPathName() {
        if ($Null -eq $this.PathName) {
            if ($Null -eq $this.Parent) {
                $this.PathName = ''
            }
            elseif ($Null -ne $this.Key) {
                $Name =
                    if     ($this.Key -is [ValueType])        { "$($this.Key)" }
                    elseif ($this.Key -isnot [String])        { "[$($this.Key.GetType())]'$($this.Key)'" }
                    elseif ($this.Key -Match '^[_,a-z]+\w*$') { "$($this.Key)" }
                    else                                      { "$($this.Key)'" }
                $this.PathName = "$($this.Parent.GetPathName()).$Name"
            }
            elseif ($Null -ne $this.Index) {
                $this.PathName = "$($this.Parent.GetPathName())[$($this.Index)]"
            }
            else { Write-Error 'Should not happen' }
        }
        return $this.PathName
    }

    [Bool]Contains($Name) {
        if ($this.Construction -eq 'Object') { return $Null -ne $this.Value.PSObject.Properties[$Name] }
        elseif ($this.Construction -in 'List', 'Dictionary') { return $this.Value.Contains($Name) }
        else { return $false }
    }

    [Object]Get($Name) {
        switch ($this.Construction) {
            Object     { return $this.Value.PSObject.Properties[$Name].Value }
            Dictionary { return $this.Value[$Name] }
            List       { return $this.Value[$Name] }
        }
        return [Management.Automation.Internal.AutomationNull]::Value
    }

    Set($Name, $Value) {
        switch ($this.Construction) {
            Object     { $this.Value.PSObject.Properties[$Name].Value = $Value } # Doesn't create new properties
            Dictionary { $this.Value[$Name] = $Value }
            List       { $this.Value[$Name] = $Value }
        }
    }

    [PSNode]GetItemNode($Key) {
        if ($this.Structure -eq 'Scalar') { Write-Error "Expected collection" }
        elseif ($this.Depth -ge $this.MaxDepth) {
            Write-Warning "$($this.GetPathName) reached the maximum depth of $($this.MaxDepth)."
        }
        elseif ($this.Structure -eq 'List') {
            $Node        = [PSNode]::new($this.Value[$Key])
            $Node.Index  = $Key
            $Node.Depth  = $this.Depth + 1
            $Node.Parent = $this
            return $Node
        }
        elseif ($this.Structure -eq 'Dictionary') {
            $Node        = [PSNode]::new($this.Get($Key))
            $Node.Key    = $Key
            $Node.Depth  = $this.Depth + 1
            $Node.Parent = $this
            return $Node
        }
        return $null
    }

    [PSNode[]]GetItemNodes() {
        $ItemNodes = [Collections.Generic.List[PSNode]]::new()
        if ($this.Structure -eq 'Scalar') { Write-Error "Expected collection" }
        elseif ($this.Depth -ge $this.MaxDepth) {
            Write-Warning "$($this.GetPathName) reached the maximum depth of $($this.MaxDepth)."
        }
        elseif ($this.Structure -eq 'List') {
            for ($i = 0; $i -lt $this.Value.Count; $i++) {
                $Node        = [PSNode]::new($this.Value[$i])
                $Node.Index  = $i
                $Node.Depth  = $this.Depth + 1
                $Node.Parent = $this
                $ItemNodes.Add($Node)
            }
        }
        elseif ($this.Structure -eq 'Dictionary') {
            if ($this.Construction -eq 'Object') { $Items = $this.Value.PSObject.Properties }
            else                                 { $Items = $this.Value.GetEnumerator() }
            $i = 0
            $Items.foreach{
                $Node        = [PSNode]::new($_.Value)
                $Node.Key    = $_.Name
                $Node.Depth  = $this.Depth + 1
                $Node.Parent = $this
                $ItemNodes.Add($Node)
            }
        }
        return $ItemNodes
    }

    [Int]get_Count() {
        switch ($this.Construction) {
            Object     { return @($this.Value.PSObject.Properties).Count }
            Dictionary { return $this.Value.get_Count() }
            List       { return $this.Value.get_Count() }
        }
        return 0
    }

    [Array]get_Keys() {
        switch ($this.Construction) {
            Object     { return $this.Value.PSObject.Properties.Name }
            Dictionary { return $this.Value.get_Keys() }
            List       { return 0..($this.Value.Length - 1) }
        }
        return [Management.Automation.Internal.AutomationNull]::Value
    }

    [Array]get_Values() {
        switch ($this.Construction) {
            Object     { return $this.Value.PSObject.Properties.Value }
            Dictionary { return $this.Value.get_Values() }
            List       { return $this.Value }
        }
        return [Management.Automation.Internal.AutomationNull]::Value
    }
}
#EndRegion './Classes/PSNode.ps1' 215
#Region './Private/Merge-Array.ps1' 0
<#
 .Synopsis
  Merges two arrays into one new array

 .Description
  This function merges two arrays into one new one.
  The values in the Merge array are overwriting any existing
  values in the Reference array.

 .Parameter Reference
  The Reference array that is used as the starting point

 .Parameter Merge
  The Merge array that will be merged into the Reference array.

 .Example
   # Merges the Merge array into the Reference array
   $reference = @(1,2,3,4,5,6,7,8,9,10)
   $merge = @(11,12,13,14,15,16,17,18,19,20)

   Merge-Array -Reference $reference -Merge $merge
#>

function Merge-Array
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Array]
        $Reference,

        [Parameter(Mandatory = $true)]
        [System.Array]
        $Merge
    )

    $script:level++
    Write-LogEntry -Message "Processing array: $($Merge.Count) items" -Level $script:level

    foreach ($item in $Merge)
    {
        switch ($item.GetType().FullName)
        {
            'System.Collections.Hashtable'
            {
                $refItem = $Reference | Where-Object -FilterScript {
                    ($_.ContainsKey('UniqueId') -and $_.UniqueId -eq $item.UniqueId) -or `
                    ($_.ContainsKey('Identity') -and $_.Identity -eq $item.Identity) -or `
                    ($_.ContainsKey('Id') -and $_.Id -eq $item.Id) -or `
                    ($_.ContainsKey('NodeName') -and $_.NodeName -eq $item.NodeName)
                }

                if ($null -eq $refItem)
                {
                    # Add item
                    Write-LogEntry -Message " Hashtable doesn't exist in Reference. Adding." -Level $script:level
                    $Reference += $item
                }
                else
                {
                    # Compare item
                    $script:level++
                    Write-LogEntry -Message 'Hashtable exists in Reference. Merging.' -Level $script:level
                    $refItem = Merge-Hashtable -Reference $refItem -Merge $item
                    $script:level--
                }
            }
            Default
            {
                if ($Reference -notcontains $item)
                {
                    $Reference += $item
                }
            }
        }
    }
    $script:level--

    return $Reference
}
#EndRegion './Private/Merge-Array.ps1' 80
#Region './Private/Merge-Hashtable.ps1' 0
<#
 .Synopsis
  Merges two hashtables

 .Description
  This function merges two hashtables into one new one.
  The values in the Merge hashtable are overwriting any existing
  values in the Reference hashtable.

 .Parameter Reference
  The Reference hashtable that is used as the starting point

 .Parameter Merge
  The Merge hashtable that will be merged into the Reference hashtable.

 .Example
   # Merges the Merge file into the Reference file
   $reference = @{
         'Key1' = 'Value1'
         'Key2' = 'Value2'
         'Key3' = @{
              'Key3.1' = 'Value3.1'
              'Key3.2' = 'Value3.2'
         }
   }
   $merge = @{
         'Key1' = 'ValueNew'
         'Key3' = @{
              'Key3.2' = 'ValueNew'
              'Key3.3' = 'Value3.3'
         }
   }

   Merge-Hashtable -Reference $reference -Merge $merge
#>

function Merge-Hashtable
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $Reference,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $Merge
    )

    $script:level++
    $items = $Merge.GetEnumerator()
    foreach ($item in $items)
    {
        $itemKey = $item.Key
        $itemData = $item.Value
        Write-LogEntry -Message "Processing: $itemKey" -Level $script:level
        switch ($itemData.GetType().FullName)
        {
            'System.Collections.Hashtable'
            {
                # Check if item exists in the reference
                if ($Reference.ContainsKey($itemKey) -eq $false)
                {
                    # item does not exist, add item
                    Write-LogEntry -Message ' Key missing in Merge object, adding key' -Level $script:level
                    $Reference.Add($itemKey, $itemData)
                }
                else
                {
                    $script:level++
                    Write-LogEntry -Message 'Key exists in Merge object, checking child items' -Level $script:level
                    $Reference.$itemKey = Merge-Hashtable -Reference $Reference.$itemKey -Merge $itemData
                    $script:level--
                }
            }
            'System.Object[]'
            {
                if ($null -eq $Reference.$itemKey -or $Reference.$itemKey.Count -eq 0)
                {
                    $Reference.$itemKey = $itemData
                }
                else
                {
                    $Reference.$itemKey = [Array](Merge-Array -Reference $Reference.$itemKey -Merge $itemData)
                }
            }
            Default
            {
                if ($Reference.$itemKey -ne $itemData)
                {
                    $Reference.$itemKey = $itemData
                }
            }
        }
    }
    $script:level--

    return $Reference
}
#EndRegion './Private/Merge-Hashtable.ps1' 99
#Region './Private/Write-LogEntry.ps1' 0
<#
 .Synopsis
  Writes a log entry to the console, including a timestamp

 .Description
  This function writes a log entry to the console, including a
  timestamp of the current time.

 .Parameter Message
  The message that has to be written to the console.

 .Parameter Level
  The number of spaces the message has to be indented.

 .Example
  Write-LogEntry -Message 'This is a log entry'

 .Example
  Write-LogEntry -Message 'This is an indented log entry' -Level 1
#>

function Write-LogEntry
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Using Write-Host to force output to the screen instead of into the pipeline.')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Message,

        [Parameter()]
        [System.Int32]
        $Level = 0
    )

    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $indentation = ' ' * $Level
    $output = '[{0}] - {1}{2}' -f $timestamp, $indentation, $Message

    Write-Host -Object $output
}
#EndRegion './Private/Write-LogEntry.ps1' 42
#Region './Public/Add-ModulesToBlobStorage.ps1' 0
function Add-ModulesToBlobStorage
{
    <#
.SYNOPSIS
    Downloads all Microsoft365DSC dependencies and uploads these to an Azure Blob Storage

.DESCRIPTION
    This function checks which dependencies the used version of Microsoft365DSC
    requires and downloads these from the PowerShell Gallery. The dependencies
    are then packaged into a zip file and uploaded to an Azure Blob Storage.

.PARAMETER ResourceGroupName
    The Azure Resource Group Name where the Storage Account is located

.PARAMETER StorageAccountName
    The name of the Storage Account where the zip file will be uploaded to

.PARAMETER ContainerName
    The name of the Container where the zip file will be uploaded to

.EXAMPLE
    Add-ModulesToBlobStorage -ResourceGroupName 'MyResourceGroup' -StorageAccountName 'MyStorageAccount' -ContainerName 'MyContainer'
#>

    [CmdletBinding()]
    param
    (
        # [Parameter(Mandatory = $true)]
        # [System.String]
        # $SubscriptionName,

        [Parameter(Mandatory = $true)]
        [System.String]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [System.String]
        $StorageAccountName,

        [Parameter(Mandatory = $true)]
        [System.String]
        $ContainerName
    )

    $script:level++
    Write-LogEntry -Message 'Upload Microsoft365DSC module dependencies to storage container' -Level $script:level

    $script:level++
    Write-LogEntry -Message "Connecting to storage account '$StorageAccountName'" -Level $script:level
    $storageAcc = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName

    Write-LogEntry -Message 'Retrieving storage account context' -Level $script:level
    $context = $storageAcc.Context

    Write-LogEntry -Message 'Checking dependencies' -Level $script:level
    $m365Module = Get-Module -Name Microsoft365DSC -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1
    $modulePath = Split-Path -Path $m365Module.Path -Parent

    $versionString = $m365Module.Version.ToString() -replace '\.', '_'

    $dependenciesPath = Join-Path -Path $modulePath -ChildPath 'Dependencies\Manifest.psd1'

    if (Test-Path -Path $dependenciesPath)
    {
        Write-LogEntry -Message 'Downloading dependencies' -Level $script:level
        $script:level++

        $destination = Join-Path -Path $env:TEMP -ChildPath 'M365DSCModules'
        $savePath = Join-Path -Path $destination -ChildPath $m365Module.Version.ToString()
        if (Test-Path -Path $savePath)
        {
            Write-LogEntry -Message "$savePath already exists. Removing!" -Level $script:level
            Remove-Item -Path $savePath -Recurse -Confirm:$false
        }
        $null = New-Item -Path $savePath -ItemType 'Directory'

        Write-LogEntry -Message ('Saving module {0} (v{1})' -f $m365Module.Name, $m365Module.Version.ToString()) -Level $script:level
        Save-Module -Name $m365Module.Name -RequiredVersion $m365Module.Version.ToString() -Path $savePath

        $data = Import-PowerShellDataFile -Path $dependenciesPath
        foreach ($dependency in $data.Dependencies)
        {
            Write-LogEntry -Message ('Saving module {0} (v{1})' -f $dependency.ModuleName, $dependency.RequiredVersion) -Level $script:level
            Save-Module -Name $dependency.ModuleName -RequiredVersion $dependency.RequiredVersion -Path $savePath
        }
        $script:level--

        Write-LogEntry -Message 'Packaging Zip file' -Level $script:level
        $zipFileName = "M365DSCDependencies-$versionString.zip"
        $zipFilePath = Join-Path -Path $env:TEMP -ChildPath $zipFileName
        if ((Test-Path -Path $zipFilePath))
        {
            $script:level++
            Write-LogEntry -Message "$zipFileName already exist on disk. Removing!" -Level $script:level
            Remove-Item -Path $zipFilePath -Confirm:$false
            $script:level--
        }
        Compress-Archive -Path $savePath\* -DestinationPath $zipFilePath

        Write-LogEntry -Message 'Uploading Zip file' -Level $script:level
        $blobContent = Get-AzStorageBlob -Container $ContainerName -Context $context -Prefix $zipFileName
        if ($null -ne $blobContent)
        {
            $script:level++
            Write-LogEntry -Message "$zipFileName already exist in the Blob Storage. Removing!" -Level $script:level
            $blobContent | Remove-AzStorageBlob
            $script:level--
        }
        $null = Set-AzStorageBlobContent -Container $ContainerName -File $zipFilePath -Context $context -Force

        Write-LogEntry -Message 'Removing temporary components' -Level $script:level
        Remove-Item -Path $savePath -Recurse -Confirm:$false -Force
        Remove-Item -Path $zipFilePath -Confirm:$false
    }
    else
    {
        Write-LogEntry -Message '[ERROR] Dependencies\Manifest.psd1 file not found' -Level $script:level
    }
    $script:level--
    $script:level--
}
#EndRegion './Public/Add-ModulesToBlobStorage.ps1' 121
#Region './Public/Compare-DataObject.ps1' 0
function Compare-DataObject
{
<#
.SYNOPSIS
    Compare Data Object

.DESCRIPTION
    Deep compares two Data Object and lists the differences between them.

.PARAMETER InputObject
    The input object that will be compared with the reference object (see: [-Reference] parameter).

    > [!NOTE]
    > Multiple input object might be provided via the pipeline.
    > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline.
    > To avoid a list of (root) objects to unroll, use the **comma operator**:

        ,$InputObject | Compare-DataObject $Reference.

.PARAMETER Reference
    The reference that is used to compared with the input object (see: [-InputObject] parameter).

.PARAMETER IsEqual
    If set, the cmdlet will return a boolean (`$true` or `$false`).
    As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties.

.PARAMETER MatchCase
    Unless the `-MatchCase` switch is provided, string values are considered case insensitive.

    > [!NOTE]
    > Dictionary keys are compared based on the `$Reference`.
    > if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison
    > is case insensitive otherwise the comparer supplied with the dictionary is used.

.PARAMETER MatchType
    Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the
    `$Reference` object is leading. Meaning `$Reference -eq $InputObject`:

        '1.0' -eq 1.0 # $false
        1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided)

.PARAMETER MatchObjectOrder
    Whether a list (or array) is treated as ordered is defined by the `$Reference`.
    Unless the `-MatchObjectOrder` switch is provided, the order of an object array (`@(...)` aka `Object[]`)
    is presumed unordered. This means that `Compare-DataObject` cmdlet will try to match each item of an
    `$InputObject` list which each item in the `$Reference` list.

    If there is a single discrepancy on each side, the properties will be compared deeper, otherwise a
    list with different items will be returned.

.PARAMETER IgnoreArrayOrder
    Whether a list (or array) is treated as ordered is defined by the `$Reference`.
    Unless the `-IgnoreArrayOrder` switch is provided, the order of an array (e.g. `[String[]]('a', b', 'c')`,
    excluding an object array, see: [-MatchObjectOrder]), is presumed ordered.

.PARAMETER IgnoreListOrder
    Whether a list is treated as ordered is defined by the `$Reference`.
    Unless the `-IgnoreListOrder` switch is provided, the order of a list
    (e.g. `[Collections.Generic.List[Int]](1, 2, 3)`), is presumed ordered.

.PARAMETER IgnoreDictionaryOrder
    Whether a dictionary is treated as ordered is defined by the `$Reference`.
    Unless the `-IgnoreDictionaryOrder` switch is provided, the order of a dictionary is presumed ordered.

    > [!WARNING]
    > A `[HashTable]` type is unordered by design and therefore the order of a `$Reference` hash table
    > in always ignored

.PARAMETER IgnorePropertyOrder
    Whether the properties are treated as ordered is defined by the `$Reference`.
    Unless the `-IgnorePropertyOrder` switch is provided, the property order is presumed ordered.

.PARAMETER MaxDepth
    The maximal depth to recursively compare each embedded property (default: 10).

.EXAMPLE
    ,$InputObject | Compare-DataObject $Reference
#>


    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseLiteralInitializerForHashtable', '', Justification = 'Using case sensitive hashtable on purpose')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Still have to check with developer')]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $true)]
        $InputObject,

        [Parameter(Mandatory = $true, Position = 0)]
        $Reference,

        [Switch]
        $IsEqual,

        [Switch]
        $MatchCase,

        [Switch]
        $MatchType,

        [Switch]
        $MatchObjectOrder,

        [Switch]
        $IgnoreArrayOrder,

        [Switch]
        $IgnoreListOrder,

        [Switch]
        $IgnoreDictionaryOrder,

        [Switch]
        $IgnorePropertyOrder,

        [Alias('Depth')]
        [int]
        $MaxDepth = 10
    )
    begin
    {
        $ReferenceNode = [PSNode]::new($Reference)
        $ReferenceNode.MaxDepth = $MaxDepth
        function CompareObject()
        {
            [CmdletBinding()]
            [OutputType([System.Boolean])]
            param (
                [PSNode]
                $ReferenceNode,

                [PSNode]
                $ObjectNode,

                [Switch]
                $IsEqual = $IsEqual
            )

            if ($MatchType)
            {
                if ($ObjectNode.Type -ne $ReferenceNode.Type)
                {
                    if ($IsEqual)
                    {
                        return $false
                    }
                    [PSCustomObject]@{
                        Path        = $ObjectNode.GetPathName()
                        Discrepancy = 'Type'
                        InputObject = $ObjectNode.Type
                        Reference   = $ReferenceNode.Type
                    }
                }
            }
            if ($ObjectNode.Structure -ne $ReferenceNode.Structure)
            {
                if ($IsEqual)
                {
                    return $false
                }
                [PSCustomObject]@{
                    Path        = $ObjectNode.GetPathName()
                    Discrepancy = 'Structure'
                    InputObject = $ObjectNode.Structure
                    Reference   = $ReferenceNode.Structure
                }
            }
            elseif ($ObjectNode.Structure -eq 'Scalar')
            {
                $NotEqual = if ($MatchCase)
                {
                    $ReferenceNode.Value -cne $ObjectNode.Value
                }
                else
                {
                    $ReferenceNode.Value -cne $ObjectNode.Value
                }
                if ($NotEqual)
                {
                    # $ReferenceNode dictates the type
                    if ($IsEqual)
                    {
                        return $false
                    }
                    [PSCustomObject]@{
                        Path        = $ObjectNode.GetPathName()
                        Discrepancy = 'Value'
                        InputObject = $ObjectNode.Value
                        Reference   = $ReferenceNode.Value
                    }
                }
            }
            else
            {
                if ($ObjectNode.get_Count() -ne $ReferenceNode.get_Count())
                {
                    if ($IsEqual)
                    {
                        return $false
                    }
                    [PSCustomObject]@{
                        Path        = $ObjectNode.GetPathName()
                        Discrepancy = 'Size'
                        InputObject = $ObjectNode.get_Count()
                        Reference   = $ReferenceNode.get_Count()
                    }
                }
                if ($ObjectNode.Structure -eq 'List')
                {
                    $ObjectItems = $ObjectNode.GetItemNodes()
                    $ReferenceItems = $ReferenceNode.GetItemNodes()
                    $MatchOrder =
                    $ObjectNode.get_Count() -eq 0 -or $ReferenceNode.get_Count() -eq 0 -or
                        ($ObjectNode.get_Count() -eq 1 -and $ReferenceNode.get_Count() -eq 1) -or
                    $(
                        if ($ReferenceNode.Type.Name -eq 'Object[]')
                        {
                            $MatchObjectOrder
                        }
                        elseif ($ReferenceNode.Value -eq [Array])
                        {
                            -not $IgnoreArrayOrder
                        }
                        else
                        {
                            -not $IgnoreListOrder
                        }
                    )
                    if ($MatchOrder)
                    {
                        $Min = [Math]::Min($ObjectNode.get_Count(), $ReferenceNode.get_Count())
                        $Max = [Math]::Max($ObjectNode.get_Count(), $ReferenceNode.get_Count())
                        for ($Index = 0; $Index -lt $Max; $Index++)
                        {
                            if ($Index -lt $Min)
                            {
                                $Compare = CompareObject -Reference $ReferenceItems[$Index] -Object $ObjectItems[$Index] -IsEqual:$IsEqual
                                if ($Compare -eq $false)
                                {
                                    return $Compare
                                }
                                elseif ($Compare -ne $true)
                                {
                                    $Compare
                                }
                            }
                            elseif ($Index -ge $ObjectNode.get_Count())
                            {
                                if ($IsEqual)
                                {
                                    return $false
                                }
                                [PSCustomObject]@{
                                    Path        = $ReferenceItems[$Index].GetPathName() # ($ObjectItem doesn't exist)
                                    Discrepancy = 'Exists'
                                    InputObject = $false
                                    Reference   = $true
                                }
                            }
                            else
                            {
                                if ($IsEqual)
                                {
                                    return $false
                                }
                                [PSCustomObject]@{
                                    Path        = $ObjectItems[$Index].GetPathName()
                                    Discrepancy = 'Exists'
                                    InputObject = $true
                                    Reference   = $false
                                }
                            }
                        }
                    }
                    else
                    {
                        $ObjectLinks = [system.collections.generic.dictionary[int, int]]::new()
                        $ReferenceLinks = [system.collections.generic.dictionary[int, int]]::new()
                        foreach ($ObjectItem in $ObjectItems)
                        {
                            $Found = $Null
                            foreach ($ReferenceItem in $ReferenceItems)
                            {
                                if (-not $ReferenceLinks.ContainsKey($ReferenceItem.Index))
                                {
                                    $Found = CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual
                                    if ($Found)
                                    {
                                        $ReferenceLinks[$ReferenceItem.Index] = $ObjectItem.Index
                                        break # Link only one reference item
                                    }
                                }
                            }
                            if ($Found)
                            {
                                $ObjectLinks[$ObjectItem.Index] = $ReferenceItem.Index
                            }
                            elseif ($IsEqual)
                            {
                                return $false
                            }
                        }
                        $MissingObjects = $ObjectItems.get_Count() - $ObjectLinks.get_Count()
                        $MissingReferences = $ReferenceItems.get_Count() - $ReferenceLinks.get_Count()
                        $Equal = -not $MissingObjects -and -not $MissingReferences
                        if ($IsEqual)
                        {
                            return $Equal
                        }
                        elseif ($Equal)
                        {
                            return
                        }
                        if ($MissingObjects -eq 1 -and $MissingReferences -eq 1)
                        {
                            $ObjectExcept = ([int[]][Linq.Enumerable]::Except([int[]]$ObjectItems.Index, [int[]]$ObjectLinks.get_Keys()))[0]
                            $ReferenceExcept = ([int[]][Linq.Enumerable]::Except([int[]]$ReferenceItems.Index, [int[]]$ReferenceLinks.get_Keys()))[0]
                            CompareObject -Reference $ReferenceItems[$ReferenceExcept] -Object $ObjectItems[$ObjectExcept] -IsEqual:$IsEqual
                        }
                        else
                        {
                            $Max = [Math]::Max($ObjectNode.get_Count(), $ReferenceNode.get_Count())
                            for ($Index = 0; $Index -lt $Max; $Index++)
                            {
                                if ($Index -ge $ObjectItems.get_Count())
                                {
                                    [PSCustomObject]@{
                                        Path        = $ReferenceNode.GetPathName() + "[$Index]"
                                        Discrepancy = 'Exists'
                                        InputObject = $false
                                        Reference   = $true
                                    }
                                }
                                elseif ($Index -ge $ReferenceItems.get_Count())
                                {
                                    [PSCustomObject]@{
                                        Path        = $ObjectNode.GetPathName() + "[$Index]"
                                        Discrepancy = 'Exists'
                                        InputObject = $true
                                        Reference   = $false
                                    }
                                }
                                elseif ($Index -notin $ObjectLinks.get_Keys())
                                {
                                    [PSCustomObject]@{
                                        Path        = $ReferenceNode.GetPathName() + "[$Index]"
                                        Discrepancy = 'Linked'
                                        InputObject = $false
                                        Reference   = $ReferenceLinks[$Index]
                                    }
                                }
                                elseif ($Index -notin $ReferenceLinks.get_Keys())
                                {
                                    [PSCustomObject]@{
                                        Path        = $ObjectNode.GetPathName() + "[$Index]"
                                        Discrepancy = 'Linked'
                                        InputObject = $ObjectLinks[$Index]
                                        Reference   = $false
                                    }
                                }
                            }
                        }
                    }
                }
                elseif ($ObjectNode.Structure -eq 'Dictionary')
                {
                    $Found = [HashTable]::new() # (Case sensitive)
                    $MatchOrder = $ReferenceNode.Type.Name -ne 'HashTable' -and $(
                        if ($ReferenceNode.Construction -eq 'Object')
                        {
                            -not $IgnorePropertyOrder
                        }
                        else
                        {
                            -not $IgnoreDictionaryOrder
                        }
                    )
                    $Order = if ($MatchOrder)
                    {
                        [HashTable]::new()
                    }
                    $Index = 0
                    if ($MatchOrder)
                    {
                        $ReferenceNode.Get_Keys().foreach{ $Order[$_] = $Index++ }
                    }
                    $Index = 0
                    foreach ($ObjectItem in $ObjectNode.GetItemNodes())
                    {
                        if ($ReferenceNode.Contains($ObjectItem.Key))
                        {
                            $ReferenceItem = $ReferenceNode.GetItemNode($ObjectItem.Key)
                            $Found[$ReferenceItem.Key] = $true
                            if ($MatchOrder -and $Order[$ReferenceItem.Key] -ne $Index)
                            {
                                if ($IsEqual)
                                {
                                    return $false
                                }
                                [PSCustomObject]@{
                                    Path        = $ObjectItem.GetPathName()
                                    Discrepancy = 'Order'
                                    InputObject = $Index
                                    Reference   = $Order[$ReferenceItem.Key]
                                }
                            }
                            $Compare = CompareObject -Reference $ReferenceItem -Object $ObjectItem -IsEqual:$IsEqual
                            if ($Compare -eq $false)
                            {
                                return $Compare
                            }
                            elseif ($Compare -ne $true)
                            {
                                $Compare
                            }
                        }
                        else
                        {
                            if ($IsEqual)
                            {
                                return $false
                            }
                            [PSCustomObject]@{
                                Path        = $ObjectItem.GetPathName()
                                Discrepancy = 'Exists'
                                InputObject = $true
                                Reference   = $false
                            }
                        }
                        $Index++
                    }
                    $ReferenceNode.get_Keys().foreach{
                        if (-not $Found.Contains($_))
                        {
                            if ($IsEqual)
                            {
                                return $false
                            }
                            [PSCustomObject]@{
                                Path        = $ReferenceNode.GetItemNode($_).GetPathName()
                                Discrepancy = 'Exists'
                                InputObject = $false
                                Reference   = $true
                            }
                        }
                    }
                }
            }
            if ($IsEqual)
            {
                return $true
            }
        }
    }
    process
    {
        CompareObject -ReferenceNode $ReferenceNode -ObjectNode $InputObject
    }
}
#EndRegion './Public/Compare-DataObject.ps1' 460
#Region './Public/ConvertTo-SortedDataObject.ps1' 0
function ConvertTo-SortedDataObject
{
    <#
.SYNOPSIS
    Sort Data Object

.DESCRIPTION
    Recursively sorts a Data Object alphabetically.

.PARAMETER InputObject
    The input object that will be recursively sorted.

    > [!NOTE]
    > Multiple input object might be provided via the pipeline.
    > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline.
    > To avoid a list of (root) objects to unroll, use the **comma operator**:

        ,$InputObject | ConvertTo-SortedDataObject.

.PARAMETER PrimaryKey
    Any primary key defined by the [-PrimaryKey] parameter will be put on top of [-InputObject]
    independent of the (descending) sort order.

    It is allowed to supply multiple primary keys.

.PARAMETER MatchCase
    Indicates that the sort is case-sensitive. By default, sorts aren't case-sensitive.

.PARAMETER Descending
    Indicates that ConvertTo-SortedDataObject sorts the objects in descending order. The default is ascending order.

    > [!NOTE]
    > Primary keys (see: [-PrimaryKey]) will always put on top.

.PARAMETER MaxDepth
    The maximal depth to recursively compare each embedded property (default: 10).

.EXAMPLE
    ,$InputObject | ConvertTo-SortedDataObject
#>


    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseLiteralInitializerForHashtable', '', Justification = 'Using case sensitive hashtable on purpose')]
    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs', '')]
    [CmdletBinding()]
    [OutputType([Object[]])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $True)]
        $InputObject,

        [Alias('By')]
        [String[]]
        $PrimaryKey,

        [Switch]
        $MatchCase,

        [Switch]
        $Descending,

        [Alias('Depth')]
        [int]
        $MaxDepth = 10
    )

    begin
    {
        $Primary = @{}
        if ($PSBoundParameters.ContainsKey('PrimaryKey'))
        {
            for ($i = 0; $i -lt $PrimaryKey.Count; $i++)
            {
                if ($Descending)
                {
                    $Primary[$PrimaryKey[$i]] = [Char]254 + '#' * ($PrimaryKey.Count - $i)
                }
                else
                {
                    $Primary[$PrimaryKey[$i]] = ' ' + '#' * $i
                }
            }
        }

        function SortObject
        {
            [CmdletBinding()]
            [OutputType([Object[]])]
            param
            (
                [Parameter()]
                [PSNode]
                $Node,

                [Switch]
                $SortIndex
            )
            if ($Node.Structure -eq 'Scalar')
            {
                $SortKey = if ($Null -eq $($Node.Value))
                {
                    [Char]27 + '$Null'
                }
                elseif ($MatchCase)
                {
                    "$($Node.Value)".ToUpper()
                }
                else
                {
                    "$($Node.Value)"
                }
                $Output = @{ $SortKey = $($Node.Value) }
            }
            elseif ($Node.Structure -eq 'List')
            {
                # This will convert the list to an (fixed) array
                $Items = $Node.GetItemNodes().foreach{ SortObject $_ -SortIndex }
                $Items = $Items | Sort-Object -CaseSensitive:$MatchCase -Descending:$Descending { $_.Keys[0] }
                $String = [Collections.Generic.List[String]]::new()
                $List = [Collections.Generic.List[Object]]::new()
                foreach ($Item in $Items)
                {
                    $SortKey = $Item.GetEnumerator().Name
                    $String.Add($SortKey)
                    $List.Add($Item[$SortKey])
                }
                $Name = $String -Join [Char]255
                $Output = @{ $Name = @($List) }
            }
            elseif ($Node.Structure -eq 'Dictionary')
            {
                # This will convert a dictionary to a PSCustomObject
                $HashTable = [HashTable]::New(0, [StringComparer]::Ordinal)
                $Node.GetItemNodes().foreach{
                    $SortObject = SortObject $_ -SortIndex
                    $SortKey = $SortObject.GetEnumerator().Name
                    if ($Primary.Contains($_.Key))
                    {
                        $Key = $Primary[$_.Key]
                    }
                    else
                    {
                        $Key = $_.Key
                    }
                    $HashTable["$Key$([Char]255)$SortKey"] = @{ $_.Key = $SortObject[$SortKey] }
                }
                $SortedKeys = $HashTable.get_Keys() | Sort-Object -CaseSensitive:$MatchCase -Descending:$Descending
                $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal)
                @($SortedKeys).foreach{
                    $Item = $HashTable[$_]
                    $Name = $Item.GetEnumerator().Name
                    $Properties[$Name] = $Item[$Name]
                }
                $Name = $SortedKeys -Join [Char]255
                $Output = @{ $Name = [PSCustomObject]$Properties } # https://github.com/PowerShell/PowerShell/issues/20753
            }
            else
            {
                Write-Error 'Should not happen'
            }
            if ($SortIndex)
            {
                $Output
            }
            else
            {
                $Output.get_Values()
            }
        }
    }

    process
    {
        $PSnode = [PSNode]::new($InputObject)
        $PSNode.MaxDepth = $MaxDepth
        SortObject -Node $PSNode
    }
}

Set-Alias -Name 'Sort-DataObject' -Value 'ConvertTo-SortedDataObject' -Scope Global
#EndRegion './Public/ConvertTo-SortedDataObject.ps1' 180
#Region './Public/Copy-DataObject.ps1' 0
function Copy-DataObject
{
<#
.SYNOPSIS
    Copy Data Object

.DESCRIPTION
    Recursively ("deep") copies a data object.

.PARAMETER InputObject
    The input object that will be recursively copied.

.PARAMETER ListAs
    If supplied, lists will be converted to the given type (or type of the supplied object example).

.PARAMETER DictionaryAs
    If supplied, dictionaries will be converted to the given type (or type of the supplied object example).
    This parameter also accepts the [`PSCustomObject`][1] types
    By default (if the [-DictionaryAs] parameters is omitted),
    [`Component`][2] objects will be converted to a [`PSCustomObject`][1] type.

.PARAMETER ExcludeLeafs
    If supplied, only the structure (lists, dictionaries, [`PSCustomObject`][1] types and [`Component`][2] types will be copied.
    If omitted, each leaf will be shallow copied

.PARAMETER MaxDepth
    The maximal depth to recursively compare each embedded property (default: 10).

.Example
    # Deep copy a complete data object into a new data object

        $NewDataObject = Copy-DataObject $DataObject

.Example
    # Copy (convert) an data object using common PowerShell arrays and PSCustomObjects

        $PSObject = Copy-DataObject $Object -ListAs [Array] -DictionaryAs PSCustomObject

.Example
    # Convert a Json string to an data object with (case insensitive) ordered dictionaries

        $PSObject = $Json | ConvertFrom-Json | Copy-DataObject -DictionaryAs ([Ordered]@{})

.LINK
    [1]: https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject "PSCustomObject Class"
    [2]: https://learn.microsoft.com/dotnet/api/system.componentmodel.component "Component Class"
#>


    [CmdletBinding(DefaultParameterSetName = 'ListAs')]
    [OutputType([Object[]])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $True)]
        $InputObject,

        [Alias('ListsAs')]
        $ListAs,

        [Alias('DictionariesAs')]
        $DictionaryAs,

        [Switch]
        $ExcludeLeafs,

        [Alias('Depth')]
        [int]
        $MaxDepth = 10
    )

    begin
    {
        function StopError($Exception, $Id = 'IncorrectArgument', $Group = [Management.Automation.ErrorCategory]::SyntaxError, $Object)
        {
            if ($Exception -is [System.Management.Automation.ErrorRecord])
            {
                $Exception = $Exception.Exception
            }
            elseif ($Exception -isnot [Exception])
            {
                $Exception = [ArgumentException]$Exception
            }
            $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new($Exception, $Id, $Group, $Object))
        }

        $ListNode = if ($PSBoundParameters.ContainsKey('ListAs'))
        {
            if ($ListAs -is [String] -or $ListAs -is [Type])
            {
                try
                {
                    [PSNode](New-Object -Type $ListAs)
                }
                catch
                {
                    StopError $_
                }
            }
            else
            {
                [PSNode]$ListAs
            }
        }

        $DictionaryNode = if ($PSBoundParameters.ContainsKey('DictionaryAs'))
        {
            if ($DictionaryAs -is [String] -or $DictionaryAs -is [Type])
            {
                try
                {
                    [PSNode](New-Object -Type $DictionaryAs)
                }
                catch
                {
                    StopError $_
                }
            }
            else
            {
                [PSNode]$DictionaryAs
            }
        }

        $ListStructure = if ($ListNode)
        {
            $ListNode.Structure
        }
        $DictionaryStructure = if ($DictionaryNode)
        {
            $DictionaryNode.Structure
        }
        if (($ListStructure -eq 'Dictionary' -and $DictionaryStructure -ne 'Dictionary') -or ($DictionaryStructure -eq 'List' -and $ListStructure -ne 'List'))
        {
            $ListNode, $DictionaryNode = $DictionaryNode, $ListNode
        }

        if ($ListNode -and $ListNode.Structure -ne 'List')
        {
            StopError 'The -ListAs parameter requires a string, type or an object example that supports a list structure'
        }
        if ($DictionaryNode -and $DictionaryNode.Structure -ne 'Dictionary')
        {
            StopError 'The -DictionaryAs parameter requires a string, type or an object example that supports a dictionary structure'
        }

        function CopyObject
        {
            [CmdletBinding()]
            [OutputType([Object[]],[System.Array])]
            param
            (
                [Parameter()]
                [PSNode]
                $Node,

                [Parameter()]
                [Type]
                $ListType,

                [Parameter()]
                [Type]
                $DictionaryType
            )

            if ($Node.Structure -eq 'Scalar')
            {
                if ($ExcludeLeafs -or $Null -eq $Node.Value)
                {
                    return $Node.Value
                }
                else
                {
                    $Node.Value.PSObject.Copy()
                }
            }
            elseif ($Node.Structure -eq 'List')
            {
                $Type = if ($Null -ne $ListType)
                {
                    $ListType
                }
                else
                {
                    $Node.Type
                }
                $Values = $Node.GetItemNodes().foreach{ CopyObject $_ -ListType $ListType -DictionaryType $DictionaryType }
                $Values = $Values -as $Type
                , $Values
            }
            elseif ($Node.Structure -eq 'Dictionary')
            {
                # This will convert a dictionary to a PSCustomObject
                $Type = if ($Null -ne $DictionaryType)
                {
                    $DictionaryType
                }
                else
                {
                    $Node.Type
                }
                $IsDirectory = $Null -ne $Type.GetInterface('IDictionary')
                if ($IsDirectory)
                {
                    $Dictionary = New-Object -Type $Type
                }
                else
                {
                    $Dictionary = [Ordered]@{}
                }
                $Node.GetItemNodes().foreach{ $Dictionary[$_.Key] = CopyObject $_ -ListType $ListType -DictionaryType $DictionaryType }
                if ($IsDirectory)
                {
                    $Dictionary
                }
                else
                {
                    [PSCustomObject]$Dictionary
                }
            }
        }
    }
    process
    {
        $PSnode = [PSNode]::new($InputObject)
        $PSNode.MaxDepth = $MaxDepth
        CopyObject -Node $PSNode -ListType $ListNode.Type -DictionaryType $DictionaryNode.Type
    }
}
#EndRegion './Public/Copy-DataObject.ps1' 228
#Region './Public/Get-ModulesFromBlobStorage.ps1' 0
function Get-ModulesFromBlobStorage
{
    <#
.SYNOPSIS
    Downloads all Microsoft365DSC dependencies from an Azure Blob Storage

.DESCRIPTION
    This function downloads the zipped dependency modules corresponding to the
    required Microsoft365DSC version from an Azure Blob Storage, if available.
    The dependencies are then unzipped and copied to the PowerShell Modules folder.

.PARAMETER ResourceGroupName
    The Azure Resource Group Name where the Storage Account is located

.PARAMETER StorageAccountName
    The name of the Storage Account where the zip file will be downloaded from

.PARAMETER ContainerName
    The name of the Container where the zip file will be downloaded from

.PARAMETER Version
    The version of the Microsoft365DSC module for which the prerequisites should be retrieved

.EXAMPLE
    Get-ModulesFromBlobStorage -ResourceGroupName 'MyResourceGroup' -StorageAccountName 'MyStorageAccount' -ContainerName 'MyContainer' -Version 1.23.530.1
#>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [System.String]
        $StorageAccountName,

        [Parameter(Mandatory = $true)]
        [System.String]
        $ContainerName,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Version
    )

    $script:level++
    Write-LogEntry -Message "Download dependencies from storage container for Microsoft365DSC v$Version." -Level $script:level

    $script:level++
    Write-LogEntry -Message "Connecting to storage account '$StorageAccountName'" -Level $script:level
    $storageAcc = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName

    Write-LogEntry -Message 'Retrieving storage account context' -Level $script:level
    $context = $storageAcc.Context

    Write-LogEntry -Message 'Checking download folder existence' -Level $script:level
    $destination = Join-Path -Path $env:TEMP -ChildPath 'M365DSCModules'
    if ((Test-Path -Path $destination) -eq $false)
    {
        $script:level++
        Write-LogEntry -Message "Creating destination folder: '$destination'" -Level $script:level
        $null = New-Item -ItemType Directory -Path $destination
        $script:level--
    }

    Write-LogEntry -Message 'Downloading blob contents from the container' -Level $script:level
    $prefix = 'M365DSCDependencies-' + ($Version -replace '\.', '_')
    $blobContent = Get-AzStorageBlob -Container $ContainerName -Context $context -Prefix $prefix

    $script:level++
    if ($null -eq $blobContent)
    {
        Write-LogEntry -Message "[ERROR] No files found that match the pattern: '$prefix'" -Level $script:level
    }
    else
    {
        Write-LogEntry -Message "Downloading $($blobContent.Name) to $destination" -Level $script:level
        $downloadFile = Join-Path -Path $destination -ChildPath $blobContent.Name
        if (Test-Path -Path $downloadFile)
        {
            $script:level++
            Write-LogEntry -Message "$downloadFile already exists. Removing!" -Level $script:level
            Remove-Item -Path $downloadFile -Confirm:$false
            $script:level--
        }
        $null = Get-AzStorageBlobContent -Container $ContainerName -Context $context -Blob $blobContent.Name -Destination $destination -Force

        Write-LogEntry -Message "Extracting $($blobContent.Name)" -Level $script:level
        $extractPath = Join-Path -Path $destination -ChildPath $Version.ToString()
        if (Test-Path -Path $extractPath)
        {
            $script:level++
            Write-LogEntry -Message "$extractPath already exists. Removing!" -Level $script:level
            Remove-Item -Path $extractPath -Recurse -Confirm:$false
            $script:level--
        }
        Expand-Archive -Path $downloadFile -DestinationPath $extractPath

        Write-LogEntry -Message "Copying modules in $extractPath to 'C:\Program Files\WindowsPowerShell\Modules'" -Level $script:level
        $downloadedModules = Get-ChildItem -Path $extractPath -Directory -ErrorAction SilentlyContinue
        foreach ($module in $downloadedModules)
        {
            $script:level++
            $PSModulePath = Join-Path -Path "$($env:ProgramFiles)/WindowsPowerShell/Modules" -ChildPath $module.Name
            if (Test-Path -Path $PSModulePath)
            {
                Write-LogEntry "Removing existing module $($module.Name)" -Level $script:level
                Remove-Item -Include '*' -Path $PSModulePath -Recurse -Force
            }

            Write-LogEntry "Deploying module $($module.Name)" -Level $script:level
            $modulePath = Join-Path -Path $extractPath -ChildPath $module.Name
            $PSModulesPath = Join-Path -Path "$($env:ProgramFiles)/WindowsPowerShell" -ChildPath 'Modules'
            Copy-Item -Path $modulePath -Destination $PSModulesPath -Recurse -Container -Force
            $script:level--
        }

        Write-LogEntry -Message 'Removing temporary components' -Level $script:level
        Remove-Item -Path $extractPath -Recurse -Confirm:$false
        Remove-Item -Path $destination -Recurse -Confirm:$false
    }
    $script:level--
    $script:level--
    $script:level--
}
#EndRegion './Public/Get-ModulesFromBlobStorage.ps1' 127
#Region './Public/Merge-DataFile.ps1' 0
function Merge-DataFile
{
<#
.SYNOPSIS
    Merges two PowerShell Data File hashtables

.DESCRIPTION
    This function merges two PowerShell Data file hashtables into one new
    one. The values in the Merge hashtable are overwriting any existing
    values in the Reference hashtable.

.PARAMETER Reference
    The Reference hashtable that is used as the starting point

.PARAMETER Merge
    The Merge hashtable that will be merged into the Reference hashtable.

.EXAMPLE
    # Merges the Merge file into the Reference file
    $reference = Import-PowerShellDataFile -Path 'reference.psd1'
    $merge = Import-PowerShellDataFile -Path 'merge.psd1'

    Merge-DataFile -Reference $reference -Merge $merge
#>

[CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $Reference,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $Merge
    )

    Begin
    {
        $script:level = 0

        Write-LogEntry -Message 'Starting Data Merge' -Level $script:level
        $ref = $Reference.Clone()
        $mer = $Merge.Clone()
    }

    Process
    {
        $result = Merge-Hashtable -Reference $ref -Merge $mer
    }

    End
    {
        Write-LogEntry -Message 'Data Merge Completed' -Level $script:level

        return $result
    }
}
#EndRegion './Public/Merge-DataFile.ps1' 58
#Region './Public/Merge-DataObject.ps1' 0
function Merge-DataObject
{
<#
.SYNOPSIS
    Merges two Data Objects into one

.DESCRIPTION
    Recursively merges two Data Object into a new Data Object.

.PARAMETER InputObject
    The input object that will be merged with the template object (see: [-Template] parameter).

    > [!NOTE]
    > Multiple input object might be provided via the pipeline.
    > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline.
    > To avoid a list of (root) objects to unroll, use the **comma operator**:

        ,$InputObject | Compare-DataObject $Template.

.PARAMETER Template
    The template that is used to merge with the input object (see: [-InputObject] parameter).

.PARAMETER PrimaryKey
    In case of a list of dictionaries or PowerShell objects, the PowerShell key is used to
    link the items or properties: if the PrimaryKey exists on both the [-Template] and the
    [-InputObject] and the values are equal, the dictionary or PowerShell object will be merged.
    Otherwise (if the key can't be found or the values differ), the complete dictionary or
    PowerShell object will be added to the list.

    It is allowed to supply multiple primary keys where each primary key will be used to
    check the relation between the [-Template] and the [-InputObject].

.PARAMETER MatchCase
    Indicates that the merge is case-sensitive. By default, merges aren't case-sensitive.

.PARAMETER MaxDepth
    The maximal depth to recursively compare each embedded property (default: 10).

.EXAMPLE
    ,$InputObject | Compare-DataObject $Template
#>


    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Scope = 'Function', Justification = 'False positive')]
    [Diagnostics.CodeAnalysis.SuppressMessage('PSReviewUnusedParameter', '', Scope = 'Function', Justification = 'Checking with Dev')]
    [CmdletBinding()]
    [OutputType([Object[]])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeLine = $True)]
        $InputObject,

        [Parameter(Mandatory = $true, Position = 0)]
        $Template,

        [String[]]
        $PrimaryKey,

        [Switch]
        $MatchCase,

        [Alias('Depth')]
        [int]
        $MaxDepth = 10
    )

    begin
    {
        $TemplateNode = [PSNode]::new($Template)
        $TemplateNode.MaxDepth = $MaxDepth
        function MergeObject
        {
            [CmdletBinding()]
            [OutputType([Object[]],[System.Array])]
            param
            (
                [Parameter()]
                [PSNode]
                $TemplateNode,

                [Parameter()]
                [PSNode]
                $ObjectNode
            )

            if ($ObjectNode.Structure -ne $TemplateNode.Structure)
            {
                return $ObjectNode.Value
            }
            elseif ($ObjectNode.Structure -eq 'Scalar')
            {
                return $ObjectNode.Value
            }
            elseif ($ObjectNode.Structure -eq 'List')
            {
                $FoundIndices = [System.Collections.Generic.HashSet[int]]::new()
                $Type = if ($ObjectNode.Value.IsFixedSize)
                {
                    [Collections.Generic.List[PSObject]]
                }
                else
                {
                    $ObjectNode.Value.GetType()
                }
                $Output = New-Object -TypeName $Type
                $ObjectItems = $ObjectNode.GetItemNodes()
                $TemplateItems = $TemplateNode.GetItemNodes()
                foreach ($ObjectItem in $ObjectItems)
                {
                    $FoundNode = $False
                    foreach ($TemplateItem in $TemplateItems)
                    {
                        if ($ObjectItem.Structure -eq $TemplateItem.Structure)
                        {
                            if ($ObjectItem.Structure -eq 'Scalar')
                            {
                                $Equal = if ($MatchCase)
                                {
                                    $TemplateItem.Value -ceq $ObjectItem.Value
                                }
                                else
                                {
                                    $TemplateItem.Value -eq $ObjectItem.Value
                                }
                                if ($Equal)
                                {
                                    $Output.Add($ObjectItem.Value)
                                    $FoundNode = $True
                                    $Null = $FoundIndices.Add($TemplateItem.Index)
                                }
                            }
                            elseif ($ObjectItem.Structure -eq 'Dictionary')
                            {
                                foreach ($Key in $PrimaryKey)
                                {
                                    if (-not $TemplateItem.Contains($Key) -or -not $ObjectItem.Contains($Key))
                                    {
                                        continue
                                    }
                                    if ($TemplateItem.Get($Key) -eq $ObjectItem.Get($Key))
                                    {
                                        $Item = MergeObject -Template $TemplateItem -Object $ObjectItem
                                        $Output.Add($Item)
                                        $FoundNode = $True
                                        $Null = $FoundIndices.Add($TemplateItem.Index)
                                    }
                                }
                            }
                        }
                    }
                    if (-not $FoundNode)
                    {
                        $Output.Add($ObjectItem.Value)
                    }
                }
                foreach ($TemplateItem in $TemplateItems)
                {
                    if (-not $FoundIndices.Contains($TemplateItem.Index))
                    {
                        $Output.Add($TemplateItem.Value)
                    }
                }
                if ($ObjectNode.Value.IsFixedSize)
                {
                    $Output = @($Output)
                }
                , $Output
            }
            elseif ($ObjectNode.Structure -eq 'Dictionary')
            {
                if ($ObjectNode.Construction -ne 'Object')
                {
                    $Dictionary = New-Object -TypeName $ObjectNode.Type
                }      # The $InputObject defines the dictionary (or PSCustomObject) type
                else
                {
                    $Dictionary = [System.Collections.Specialized.OrderedDictionary]::new()
                }
                foreach ($ObjectItem in $ObjectNode.GetItemNodes())
                {
                    if ($TemplateNode.Contains($ObjectItem.Key))
                    {
                        # The $InputObject defines the comparer
                        $Value = MergeObject -Template $TemplateNode.GetItemNode($ObjectItem.Key) -Object $ObjectItem
                    }
                    else
                    {
                        $Value = $ObjectItem.Value
                    }
                    $Dictionary.Add($ObjectItem.Key, $Value)
                }
                foreach ($Key in $TemplateNode.get_Keys())
                {
                    if (-not $Dictionary.Contains($Key))
                    {
                        $Dictionary.Add($Key, $TemplateNode.Get($Key))
                    }
                }
                if ($ObjectNode.Construction -ne 'Object')
                {
                    $Dictionary
                }
                else
                {
                    [PSCustomObject]$Dictionary
                }
            }
        }
    }
    process
    {
        MergeObject -TemplateNode $TemplateNode -ObjectNode $InputObject
    }
}
#EndRegion './Public/Merge-DataObject.ps1' 214