commands.ps1


<#
    .SYNOPSIS
        Copy files with filter parameters
         
    .DESCRIPTION
        Copy files with filter parameters
         
    .PARAMETER Source
        The source path of copying files
         
    .PARAMETER Target
        The destination path of copying files
         
    .PARAMETER Filter
        The filter parameter
         
    .EXAMPLE
        PS C:\> Copy-Filtered -Source "c:\temp\source" -Target "c:\temp\target" -Filter *.*
         
        This will build copy all the files to the destination folder
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Copy-Filtered {
    param (
        [Parameter(Mandatory = $true)]
        [string] $Source,
        [Parameter(Mandatory = $true)]
        [string] $Target,
        [Parameter(Mandatory = $true)]
        [string[]] $Filter
    )
    $ResolvedSource = Resolve-Path $Source
    $NormalizedSource = $ResolvedSource.Path.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
    Get-ChildItem $Source -Include $Filter -Recurse | ForEach-Object {
        $RelativeItemSource = $_.FullName.Replace($NormalizedSource, '')
        $ItemTarget = Join-Path $Target $RelativeItemSource
        $ItemTargetDir = Split-Path $ItemTarget
        if (!(Test-Path $ItemTargetDir)) {
            [void](New-Item $ItemTargetDir -Type Directory)
        }
        Copy-Item $_.FullName $ItemTarget
    }
}


<#
    .SYNOPSIS
        Finds files using match patterns.
         
    .DESCRIPTION
        Determines the find root from a list of patterns. Performs the find and then applies the glob patterns. Supports interleaved exclude patterns. Unrooted patterns are rooted using defaultRoot, unless matchOptions.matchBase is specified and the pattern is a basename only. For matchBase cases, the defaultRoot is used as the find root.
         
    .PARAMETER DefaultRoot
        Default path to root unrooted patterns. Falls back to System.DefaultWorkingDirectory or current location.
         
    .PARAMETER Pattern
        Patterns to apply. Supports interleaved exclude patterns.
         
    .PARAMETER FindOptions
        When the FindOptions parameter is not specified, defaults to (New-FindOptions -FollowSymbolicLinksTrue). Following soft links is generally appropriate unless deleting files.
         
    .PARAMETER MatchOptions
        When the MatchOptions parameter is not specified, defaults to (New-MatchOptions -Dot -NoBrace -NoCase).
         
    .EXAMPLE
        PS C:\> Find-FSCPSMatch -DefaultRoot "c:\temp\PackagesLocalDirectory" -Pattern '*.*' -FindOptions FollowSymbolicLinksTrue
         
        This will return all files
         
    .NOTES
        This if refactored Find-VSTSMatch function
#>

function Find-FSCPSMatch {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")]
    [OutputType('System.Object[]')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$DefaultRoot,
        [Parameter()]
        [string[]]$Pattern,
        $FindOptions,
        $MatchOptions)

    begin{
        Invoke-TimeSignal -Start
        $ErrorActionPreference = 'Stop'        
        Write-PSFMessage -Level Verbose -Message "DefaultRoot: '$DefaultRoot'"

        ##===========================internal functions start==========================##
        function New-FindOptions {
            [CmdletBinding()]
            param(
                [switch]$FollowSpecifiedSymbolicLink,
                [switch]$FollowSymbolicLinks)
        
            return New-Object psobject -Property @{
                FollowSpecifiedSymbolicLink = $FollowSpecifiedSymbolicLink.IsPresent
                FollowSymbolicLinks = $FollowSymbolicLinks.IsPresent
            }
        }
        function New-MatchOptions {
            [CmdletBinding()]
            param(
                [switch]$Dot,
                [switch]$FlipNegate,
                [switch]$MatchBase,
                [switch]$NoBrace,
                [switch]$NoCase,
                [switch]$NoComment,
                [switch]$NoExt,
                [switch]$NoGlobStar,
                [switch]$NoNegate,
                [switch]$NoNull)
        
            return New-Object psobject -Property @{
                Dot = $Dot.IsPresent
                FlipNegate = $FlipNegate.IsPresent
                MatchBase = $MatchBase.IsPresent
                NoBrace = $NoBrace.IsPresent
                NoCase = $NoCase.IsPresent
                NoComment = $NoComment.IsPresent
                NoExt = $NoExt.IsPresent
                NoGlobStar = $NoGlobStar.IsPresent
                NoNegate = $NoNegate.IsPresent
                NoNull = $NoNull.IsPresent
            }
        }
        function ConvertTo-NormalizedSeparators {
            [CmdletBinding()]
            param([string]$Path)
        
            # Convert slashes.
            $Path = "$Path".Replace('/', '\')
        
            # Remove redundant slashes.
            $isUnc = $Path -match '^\\\\+[^\\]'
            $Path = $Path -replace '\\\\+', '\'
            if ($isUnc) {
                $Path = '\' + $Path
            }
        
            return $Path
        }
        function Get-FindInfoFromPattern {
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [string]$DefaultRoot,
                [Parameter(Mandatory = $true)]
                [string]$Pattern,
                [Parameter(Mandatory = $true)]
                $MatchOptions)
        
            if (!$MatchOptions.NoBrace) {
                throw "Get-FindInfoFromPattern expected MatchOptions.NoBrace to be true."
            }
        
            # For the sake of determining the find path, pretend NoCase=false.
            $MatchOptions = Copy-MatchOptions -Options $MatchOptions
            $MatchOptions.NoCase = $false
        
            # Check if basename only and MatchBase=true
            if ($MatchOptions.MatchBase -and
                !(Test-Rooted -Path $Pattern) -and
                ($Pattern -replace '\\', '/').IndexOf('/') -lt 0) {
        
                return New-Object psobject -Property @{
                    AdjustedPattern = $Pattern
                    FindPath = $DefaultRoot
                    StatOnly = $false
                }
            }
        
            # The technique applied by this function is to use the information on the Minimatch object determine
            # the findPath. Minimatch breaks the pattern into path segments, and exposes information about which
            # segments are literal vs patterns.
            #
            # Note, the technique currently imposes a limitation for drive-relative paths with a glob in the
            # first segment, e.g. C:hello*/world. It's feasible to overcome this limitation, but is left unsolved
            # for now.
            $minimatchObj = New-Object Minimatch.Minimatcher($Pattern, (ConvertTo-MinimatchOptions -Options $MatchOptions))
        
            # The "set" field is a two-dimensional enumerable of parsed path segment info. The outer enumerable should only
            # contain one item, otherwise something went wrong. Brace expansion can result in multiple items in the outer
            # enumerable, but that should be turned off by the time this function is reached.
            #
            # Note, "set" is a private field in the .NET implementation but is documented as a feature in the nodejs
            # implementation. The .NET implementation is a port and is by a different author.
            $setFieldInfo = $minimatchObj.GetType().GetField('set', 'Instance,NonPublic')
            [object[]]$set = $setFieldInfo.GetValue($minimatchObj)
            if ($set.Count -ne 1) {
                throw "Get-FindInfoFromPattern expected Minimatch.Minimatcher(...).set.Count to be 1. Actual: '$($set.Count)'"
            }
        
            [string[]]$literalSegments = @( )
            [object[]]$parsedSegments = $set[0]
            foreach ($parsedSegment in $parsedSegments) {
                if ($parsedSegment.GetType().Name -eq 'LiteralItem') {
                    # The item is a LiteralItem when the original input for the path segment does not contain any
                    # unescaped glob characters.
                    $literalSegments += $parsedSegment.Source;
                    continue
                }
        
                break;
            }
        
            # Join the literal segments back together. Minimatch converts '\' to '/' on Windows, then squashes
            # consequetive slashes, and finally splits on slash. This means that UNC format is lost, but can
            # be detected from the original pattern.
            $joinedSegments = [string]::Join('/', $literalSegments)
            if ($joinedSegments -and ($Pattern -replace '\\', '/').StartsWith('//')) {
                $joinedSegments = '/' + $joinedSegments # restore UNC format
            }
        
            # Determine the find path.
            $findPath = ''
            if ((Test-Rooted -Path $Pattern)) { # The pattern is rooted.
                $findPath = $joinedSegments
            } elseif ($joinedSegments) { # The pattern is not rooted, and literal segements were found.
                $findPath = [System.IO.Path]::Combine($DefaultRoot, $joinedSegments)
            } else { # The pattern is not rooted, and no literal segements were found.
                $findPath = $DefaultRoot
            }
        
            # Clean up the path.
            if ($findPath) {
                $findPath = [System.IO.Path]::GetDirectoryName(([System.IO.Path]::Combine($findPath, '_'))) # Hack to remove unnecessary trailing slash.
                $findPath = ConvertTo-NormalizedSeparators -Path $findPath
            }
        
            return New-Object psobject -Property @{
                AdjustedPattern = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $Pattern
                FindPath = $findPath
                StatOnly = $literalSegments.Count -eq $parsedSegments.Count
            }
        }
        function Get-FindResult {
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [string]$Path,
                [Parameter(Mandatory = $true)]
                $Options)
        
            if (!(Test-Path -LiteralPath $Path)) {
                Write-PSFMessage -Level Verbose -Message 'Path not found.'
                return
            }
        
            $Path = ConvertTo-NormalizedSeparators -Path $Path
        
            # Push the first item.
            [System.Collections.Stack]$stack = New-Object System.Collections.Stack
            $stack.Push((Get-Item -LiteralPath $Path))
        
            $count = 0
            while ($stack.Count) {
                # Pop the next item and yield the result.
                $item = $stack.Pop()
                $count++
                $item.FullName
        
                # Traverse.
                if (($item.Attributes -band 0x00000010) -eq 0x00000010) { # Directory
                    if (($item.Attributes -band 0x00000400) -ne 0x00000400 -or # ReparsePoint
                        $Options.FollowSymbolicLinks -or
                        ($count -eq 1 -and $Options.FollowSpecifiedSymbolicLink)) {
        
                        $childItems = @( Get-ChildItem -Path "$($Item.FullName)/*" -Force )
                        [System.Array]::Reverse($childItems)
                        foreach ($childItem in $childItems) {
                            $stack.Push($childItem)
                        }
                    }
                }
            }
        }
        function Get-RootedPattern {
            [OutputType('System.String')]
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [string]$DefaultRoot,
                [Parameter(Mandatory = $true)]
                [string]$Pattern)
        
            if ((Test-Rooted -Path $Pattern)) {
                return $Pattern
            }
        
            # Normalize root.
            $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot
        
            # Escape special glob characters.
            $DefaultRoot = $DefaultRoot -replace '(\[)(?=[^\/]+\])', '[[]' # Escape '[' when ']' follows within the path segment
            $DefaultRoot = $DefaultRoot.Replace('?', '[?]')     # Escape '?'
            $DefaultRoot = $DefaultRoot.Replace('*', '[*]')     # Escape '*'
            $DefaultRoot = $DefaultRoot -replace '\+\(', '[+](' # Escape '+('
            $DefaultRoot = $DefaultRoot -replace '@\(', '[@]('  # Escape '@('
            $DefaultRoot = $DefaultRoot -replace '!\(', '[!]('  # Escape '!('
        
            if ($DefaultRoot -like '[A-Z]:') { # e.g. C:
                return "$DefaultRoot$Pattern"
            }
        
            # Ensure root ends with a separator.
            if (!$DefaultRoot.EndsWith('\')) {
                $DefaultRoot = "$DefaultRoot\"
            }
        
            return "$DefaultRoot$Pattern"
        }
        function Test-Rooted {
            [OutputType('System.Boolean')]
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [string]$Path)
        
            $Path = ConvertTo-NormalizedSeparators -Path $Path
            return $Path.StartsWith('\') -or # e.g. \ or \hello or \\hello
                $Path -like '[A-Z]:*'        # e.g. C: or C:\hello
        }
        function Copy-MatchOptions {
            [CmdletBinding()]
            param($Options)
        
            return New-Object psobject -Property @{
                Dot = $Options.Dot -eq $true
                FlipNegate = $Options.FlipNegate -eq $true
                MatchBase = $Options.MatchBase -eq $true
                NoBrace = $Options.NoBrace -eq $true
                NoCase = $Options.NoCase -eq $true
                NoComment = $Options.NoComment -eq $true
                NoExt = $Options.NoExt -eq $true
                NoGlobStar = $Options.NoGlobStar -eq $true
                NoNegate = $Options.NoNegate -eq $true
                NoNull = $Options.NoNull -eq $true
            }
        }
        function ConvertTo-MinimatchOptions {
            [CmdletBinding()]
            param($Options)
        
            $opt = New-Object Minimatch.Options
            $opt.AllowWindowsPaths = $true
            $opt.Dot = $Options.Dot -eq $true
            $opt.FlipNegate = $Options.FlipNegate -eq $true
            $opt.MatchBase = $Options.MatchBase -eq $true
            $opt.NoBrace = $Options.NoBrace -eq $true
            $opt.NoCase = $Options.NoCase -eq $true
            $opt.NoComment = $Options.NoComment -eq $true
            $opt.NoExt = $Options.NoExt -eq $true
            $opt.NoGlobStar = $Options.NoGlobStar -eq $true
            $opt.NoNegate = $Options.NoNegate -eq $true
            $opt.NoNull = $Options.NoNull -eq $true
            return $opt
        }
        function Get-LocString {
            [OutputType('System.String')]
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true, Position = 1)]
                [string]$Key,
                [Parameter(Position = 2)]
                [object[]]$ArgumentList = @( ))
        
            # Due to the dynamically typed nature of PowerShell, a single null argument passed
            # to an array parameter is interpreted as a null array.
            if ([object]::ReferenceEquals($null, $ArgumentList)) {
                $ArgumentList = @( $null )
            }
        
            # Lookup the format string.
            $format = ''
            if (!($format = $script:resourceStrings[$Key])) {
                # Warn the key was not found. Prevent recursion if the lookup key is the
                # "string resource key not found" lookup key.
                $resourceNotFoundKey = 'PSLIB_StringResourceKeyNotFound0'
                if ($key -ne $resourceNotFoundKey) {
                    Write-PSFMessage -Level Warning -Message (Get-LocString -Key $resourceNotFoundKey -ArgumentList $Key)
                }
        
                # Fallback to just the key itself if there aren't any arguments to format.
                if (!$ArgumentList.Count) { return $key }
        
                # Otherwise fallback to the key followed by the arguments.
                $OFS = " "
                return "$key $ArgumentList"
            }
        
            # Return the string if there aren't any arguments to format.
            if (!$ArgumentList.Count) { return $format }
        
            try {
                [string]::Format($format, $ArgumentList)
            } catch {
                Write-PSFMessage -Level Warning -Message (Get-LocString -Key 'PSLIB_StringFormatFailed')
                $OFS = " "
                "$format $ArgumentList"
            }
        }
        function ConvertFrom-LongFormPath {
            [OutputType('System.String')]
            [CmdletBinding()]
            param([string]$Path)
        
            if ($Path) {
                if ($Path.StartsWith('\\?\UNC')) {
                    # E.g. \\?\UNC\server\share -> \\server\share
                    return $Path.Substring(1, '\?\UNC'.Length)
                } elseif ($Path.StartsWith('\\?\')) {
                    # E.g. \\?\C:\directory -> C:\directory
                    return $Path.Substring('\\?\'.Length)
                }
            }
        
            return $Path
        }
        function ConvertTo-LongFormPath {
            [OutputType('System.String')]
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [string]$Path)
        
            [string]$longFormPath = Get-FullNormalizedPath -Path $Path
            if ($longFormPath -and !$longFormPath.StartsWith('\\?')) {
                if ($longFormPath.StartsWith('\\')) {
                    # E.g. \\server\share -> \\?\UNC\server\share
                    return "\\?\UNC$($longFormPath.Substring(1))"
                } else {
                    # E.g. C:\directory -> \\?\C:\directory
                    return "\\?\$longFormPath"
                }
            }
        
            return $longFormPath
        }
        function Get-FullNormalizedPath {
            [OutputType('System.String')]
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [string]$Path)
        
            [string]$outPath = $Path
            [uint32]$bufferSize = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, 0, $null, $null)
            [int]$lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
            if ($bufferSize -gt 0) {
                $absolutePath = New-Object System.Text.StringBuilder([int]$bufferSize)
                [uint32]$length = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, $bufferSize, $absolutePath, $null)
                $lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
                if ($length -gt 0) {
                    $outPath = $absolutePath.ToString()
                } else  {
                    throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @(
                        $lastWin32Error
                        Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path
                    ))
                }
            } else {
                throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @(
                    $lastWin32Error
                    Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path
                ))
            }
        
            if ($outPath.EndsWith('\') -and !$outPath.EndsWith(':\')) {
                $outPath = $outPath.TrimEnd('\')
            }
        
            $outPath
        }
        ##===========================internal functions end============================##
    }
    process
    {
        try 
        {            
            if (!$FindOptions) {
                $FindOptions = New-FindOptions -FollowSpecifiedSymbolicLink -FollowSymbolicLinks
            }
    
    
            if (!$MatchOptions) {
                $MatchOptions = New-MatchOptions -Dot -NoBrace -NoCase
            }
    
            $miscFolder = (Join-Path $script:ModuleRoot "\internal\misc")        
            [string]$code = Get-Content "$miscFolder\Minimatch.cs" -Raw
            Add-Type -TypeDefinition $code -Language CSharp 
    
            # Normalize slashes for root dir.
            $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot
    
            $results = @{ }
            $originalMatchOptions = $MatchOptions
            foreach ($pat in $Pattern) {
                Write-PSFMessage -Level Verbose -Message "Pattern: '$pat'"
    
                # Trim and skip empty.
                $pat = "$pat".Trim()
                if (!$pat) {
                    Write-PSFMessage -Level Verbose -Message 'Skipping empty pattern.'
                    continue
                }
    
                # Clone match options.
                $MatchOptions = Copy-MatchOptions -Options $originalMatchOptions
    
                # Skip comments.
                if (!$MatchOptions.NoComment -and $pat.StartsWith('#')) {
                    Write-PSFMessage -Level Verbose -Message 'Skipping comment.'
                    continue
                }
    
                # Set NoComment. Brace expansion could result in a leading '#'.
                $MatchOptions.NoComment = $true
    
                # Determine whether pattern is include or exclude.
                $negateCount = 0
                if (!$MatchOptions.NoNegate) {
                    while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') {
                        $negateCount++
                    }
    
                    $pat = $pat.Substring($negateCount) # trim leading '!'
                    if ($negateCount) {
                        Write-PSFMessage -Level Verbose -Message "Trimmed leading '!'. Pattern: '$pat'"
                    }
                }
    
                $isIncludePattern = $negateCount -eq 0 -or
                    ($negateCount % 2 -eq 0 -and !$MatchOptions.FlipNegate) -or
                    ($negateCount % 2 -eq 1 -and $MatchOptions.FlipNegate)
    
                # Set NoNegate. Brace expansion could result in a leading '!'.
                $MatchOptions.NoNegate = $true
                $MatchOptions.FlipNegate = $false
    
                # Trim and skip empty.
                $pat = "$pat".Trim()
                if (!$pat) {
                    Write-PSFMessage -Level Verbose -Message 'Skipping empty pattern.'
                    continue
                }
    
                # Expand braces - required to accurately interpret findPath.
                $expanded = $null
                $preExpanded = $pat
                if ($MatchOptions.NoBrace) {
                    $expanded = @( $pat )
                } else {
                    # Convert slashes on Windows before calling braceExpand(). Unfortunately this means braces cannot
                    # be escaped on Windows, this limitation is consistent with current limitations of minimatch (3.0.3).
                    Write-PSFMessage -Level Verbose -Message "Expanding braces."
                    $convertedPattern = $pat -replace '\\', '/'
                    $expanded = [Minimatch.Minimatcher]::BraceExpand(
                        $convertedPattern,
                        (ConvertTo-MinimatchOptions -Options $MatchOptions))
                }
    
                # Set NoBrace.
                $MatchOptions.NoBrace = $true
    
                foreach ($pat in $expanded) {
                    if ($pat -ne $preExpanded) {
                        Write-PSFMessage -Level Verbose -Message "Pattern: '$pat'"
                    }
    
                    # Trim and skip empty.
                    $pat = "$pat".Trim()
                    if (!$pat) {
                        Write-PSFMessage -Level Verbose -Message "Skipping empty pattern."
                        continue
                    }
    
                    if ($isIncludePattern) {
                        # Determine the findPath.
                        $findInfo = Get-FindInfoFromPattern -DefaultRoot $DefaultRoot -Pattern $pat -MatchOptions $MatchOptions
                        $findPath = $findInfo.FindPath
                        Write-PSFMessage -Level Verbose -Message "FindPath: '$findPath'"
    
                        if (!$findPath) {
                            Write-PSFMessage -Level Verbose -Message "Skipping empty path."
                            continue
                        }
    
                        # Perform the find.
                        Write-PSFMessage -Level Verbose -Message "StatOnly: '$($findInfo.StatOnly)'"
                        [string[]]$findResults = @( )
                        if ($findInfo.StatOnly) {
                            # Simply stat the path - all path segments were used to build the path.
                            if ((Test-Path -LiteralPath $findPath)) {
                                $findResults += $findPath
                            }
                        } else {
                            $findResults = Get-FindResult -Path $findPath -Options $FindOptions
                        }
    
                        Write-PSFMessage -Level Verbose -Message "Found $($findResults.Count) paths."
    
                        # Apply the pattern.
                        Write-PSFMessage -Level Verbose -Message "Applying include pattern."
                        if ($findInfo.AdjustedPattern -ne $pat) {
                            Write-PSFMessage -Level Verbose -Message "AdjustedPattern: '$($findInfo.AdjustedPattern)'"
                            $pat = $findInfo.AdjustedPattern
                        }
    
                        $matchResults = [Minimatch.Minimatcher]::Filter(
                            $findResults,
                            $pat,
                            (ConvertTo-MinimatchOptions -Options $MatchOptions))
    
                        # Union the results.
                        $matchCount = 0
                        foreach ($matchResult in $matchResults) {
                            $matchCount++
                            $results[$matchResult.ToUpperInvariant()] = $matchResult
                        }
    
                        Write-PSFMessage -Level Verbose -Message "$matchCount matches"
                    } else {
                        # Check if basename only and MatchBase=true.
                        if ($MatchOptions.MatchBase -and
                            !(Test-Rooted -Path $pat) -and
                            ($pat -replace '\\', '/').IndexOf('/') -lt 0) {
    
                            # Do not root the pattern.
                            Write-PSFMessage -Level Verbose -Message "MatchBase and basename only."
                        } else {
                            # Root the exclude pattern.
                            $pat = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $pat
                            Write-PSFMessage -Level Verbose -Message "After Get-RootedPattern, pattern: '$pat'"
                        }
    
                        # Apply the pattern.
                        Write-PSFMessage -Level Verbose -Message 'Applying exclude pattern.'
                        $matchResults = [Minimatch.Minimatcher]::Filter(
                            [string[]]$results.Values,
                            $pat,
                            (ConvertTo-MinimatchOptions -Options $MatchOptions))
    
                        # Subtract the results.
                        $matchCount = 0
                        foreach ($matchResult in $matchResults) {
                            $matchCount++
                            $results.Remove($matchResult.ToUpperInvariant())
                        }
    
                        Write-PSFMessage -Level Verbose -Message "$matchCount matches"
                    }
                }
            }
    
            $finalResult = @( $results.Values | Sort-Object )
            Write-PSFMessage -Level Verbose -Message  "$($finalResult.Count) final results"
            return $finalResult
        }         
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while finding-matches" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
        finally{
            
        }
    }
    END {
        Invoke-TimeSignal -End
    }   
    
}


<#
    .SYNOPSIS
        Load all necessary information about the D365 instance
         
    .DESCRIPTION
        Load all servicing dll files from the D365 instance into memory
         
    .EXAMPLE
        PS C:\> Get-ApplicationEnvironment
         
        This will load all the different dll files into memory.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Get-ApplicationEnvironment {
    [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList"

    $AOSPath = Join-Path $script:ServiceDrive "\AOSService\webroot\bin"    
    Write-PSFMessage -Level Verbose -Message "AOSPath $AOSPath"
    Write-PSFMessage -Level Verbose -Message "Testing if we are running on a AOS server or not."
    if (-not (Test-Path -Path $AOSPath -PathType Container)) {
        Write-PSFMessage -Level Verbose -Message "The machine is NOT an AOS server."
        
        $MRPath = Join-Path $script:ServiceDrive "MRProcessService\MRInstallDirectory\Server\Services"
        
        Write-PSFMessage -Level Verbose -Message "Testing if we are running on a BI / MR server or not."
        if (-not (Test-Path -Path $MRPath -PathType Container)) {
            Write-PSFMessage -Level Verbose -Message "It seems that you ran this cmdlet on a machine that doesn't have the assemblies needed to obtain system details. Most likely you ran it on a <c='em'>personal workstation / personal computer</c>."
            return
        }
        else {
            Write-PSFMessage -Level Verbose -Message "The machine is a BI / MR server."
            $BasePath = $MRPath

            $null = $Files2Process.Add((Join-Path $script:ServiceDrive "Monitoring\Instrumentation\Microsoft.Dynamics.AX.Authentication.Instrumentation.dll"))
        }
    }
    else {
        Write-PSFMessage -Level Verbose -Message "The machine is an AOS server."
        $BasePath = $AOSPath

        $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Authentication.Instrumentation.dll"))
    }

    Write-PSFMessage -Level Verbose -Message "Shadow cloning all relevant assemblies to the Microsoft.Dynamics.ApplicationPlatform.Environment.dll to avoid locking issues. This enables us to install updates while having fscps.tools loaded"
        
    $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Configuration.Base.dll"))
    $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.BusinessPlatform.SharedTypes.dll"))
    $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll"))
    $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Security.Instrumentation.dll"))
    $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.ApplicationPlatform.Environment.dll"))
    
    Import-AssemblyFileIntoMemory -Path $($Files2Process.ToArray()) -UseTempFolder

    if (Test-PSFFunctionInterrupt) { return }

    Write-PSFMessage -Level Verbose -Message "All assemblies loaded. Getting environment details."
    $environment = [Microsoft.Dynamics.ApplicationPlatform.Environment.EnvironmentFactory]::GetApplicationEnvironment()
    
    $environment
}


<#
    .SYNOPSIS
        Function to receive the Name of the model from descriptor
         
    .DESCRIPTION
        Function to receive the Name of the model from descriptor
         
    .PARAMETER _modelName
        Model name
         
    .PARAMETER _modelPath
        Model path
         
    .EXAMPLE
        PS C:\> Get-AXModelName ModelName "TestModel" ModelPath "c:\Temp\PackagesLocalDirectory"
         
        This will return the model name from descriptor
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Get-AXModelName {
    param (
        [Alias('ModelName')]
        [string]$_modelName,
        [Alias('ModelPath')]
        [string]$_modelPath
    )
    process{
        $descriptorSearchPath = (Join-Path $_modelPath (Join-Path $_modelName "Descriptor"))
        if(Test-Path $descriptorSearchPath)
        {
            $descriptor = (Get-ChildItem -Path $descriptorSearchPath -Filter '*.xml')
            Write-PSFMessage -Level Verbose -Message "Descriptor found at $descriptor"
            [xml]$xmlData = Get-Content $descriptor.FullName
            $modelDisplayName = $xmlData.SelectNodes("//AxModelInfo/Name")
            return $modelDisplayName.InnerText
        }
        else {
            return $null;
        }
    }
}


<#
    .SYNOPSIS
        Clone a hashtable
         
    .DESCRIPTION
        Create a deep clone of a hashtable for you to work on it without updating the original object
         
    .PARAMETER InputObject
        The hashtable you want to clone
         
    .EXAMPLE
        PS C:\> Get-DeepClone -InputObject $HashTable
         
        This will clone the $HashTable variable into a new object and return it to you.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Get-DeepClone {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
    [CmdletBinding()]
    param(
        [parameter(Mandatory = $true)]
        $InputObject
    )
    process
    {
        if($InputObject -is [hashtable]) {

            $clone = @{}

            foreach($key in $InputObject.keys)
            {
                if($key -eq "EnableException") {continue}
                
                $clone[$key] = Get-DeepClone $InputObject[$key]
            }

            $clone
        } else {
            $InputObject
        }
    }
}


<#
    .SYNOPSIS
        Get list of the D365FSC models from metadata path
         
    .DESCRIPTION
        Get list of the D365FSC models from metadata path prepared to build
         
    .PARAMETER MetadataPath
        Path to the metadata folder (PackagesLocalDirectory)
         
    .PARAMETER IncludeTest
        Includes test models
         
    .PARAMETER All
        Return all models even without source code
         
    .EXAMPLE
        PS C:\> Get-FSCModels -MetadataPath "J:\AosService\PackagesLocalDirectory"
         
        This will return the list of models without test models and models without source code
         
    .EXAMPLE
        PS C:\> Get-FSCModels -MetadataPath "J:\AosService\PackagesLocalDirectory" -IncludeTest
         
        This will return the list of models with test models and models without source code
         
    .EXAMPLE
        PS C:\> Get-FSCModels -MetadataPath "J:\AosService\PackagesLocalDirectory" -IncludeTest -All
         
        This will return the list of all models
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Get-FSCModelList
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]  $MetadataPath,
        [switch]  $IncludeTest = $false,
        [switch]  $All = $false
    )
    if(Test-Path "$MetadataPath")
    {
        $modelsList = @()

        (Get-ChildItem -Directory "$MetadataPath") | ForEach-Object {

            $testModel = ($_.BaseName -match "Test")

            if ($testModel -and $IncludeTest) {
                $modelsList += ($_.BaseName)
            }
            if((Test-Path ("$MetadataPath/$($_.BaseName)/Descriptor")) -and !$testModel) {
                $modelsList += ($_.BaseName)
            }
            if(!(Test-Path ("$MetadataPath/$($_.BaseName)/Descriptor")) -and !$testModel -and $All) {
                $modelsList += ($_.BaseName)
            }
        }
        return $modelsList -join ","
    }
    else 
    {
        Write-PSFMessage -Level Host -Message "Something went wrong while downloading NuGet package" -Exception "Folder $MetadataPath with metadata doesnot exists"
        Stop-PSFFunction -Message "Stopping because of errors"
        return
    }
}


<#
    .SYNOPSIS
        Get the list of D365FSC components versions
         
    .DESCRIPTION
        Get the list of D365FSC components versions (NuGets, Packages, Frameworks etc.)
         
         
    .PARAMETER ModelsList
        The list of D365FSC models
         
    .PARAMETER MetadataPath
        The path to the D365FSC metadata
         
    .EXAMPLE
        PS C:\> Get-FSCMTestModel -ModelsList "test" $MetadataPath "c:\temp\Metadata"
         
        This will show the list of test models.
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Get-FSCMTestModel
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $ModelsList,
        [Parameter(Mandatory = $true)]
        [string] $MetadataPath
    )
    begin{
        $testModelsList = @()
        function Get-AXModelReference
        {
            [CmdletBinding()]
            param (
                [string]
                $descriptorPath
            )
            if(Test-Path "$descriptorPath")
            {
                [xml]$xmlData = Get-Content $descriptorPath
                $modelDisplayName = $xmlData.SelectNodes("//AxModelInfo/ModuleReferences")
                return $modelDisplayName.string 
            }
        }
    }
    process{
        $ModelsList.Split(",") | ForEach-Object {
            $modelName = $_
            (Get-ChildItem -Path $MetadataPath) | ForEach-Object{ 
                $mdlName = $_.BaseName        
                if($mdlName -eq $modelName){ return; } 
                $checkTest = $($mdlName.Contains("Test"))
                if(-not $checkTest){ return; }        
                Write-PSFMessage -Level Debug -Message "ModelName: $mdlName"
                $descriptorSearchPath = (Join-Path $_.FullName "Descriptor")
                $descriptor = (Get-ChildItem -Path $descriptorSearchPath -Filter '*.xml')
                if($descriptor)
                {
                    $refmodels = (Get-AXModelReference -descriptorPath $descriptor.FullName)
                    Write-PSFMessage -Level Debug -Message "RefModels: $refmodels"
                    foreach($ref in $refmodels)
                    {
                        if($modelName -eq $ref)
                        {
                            if(-not $testModelsList.Contains("$mdlName"))
                            {
                                $testModelsList += ("$mdlName")
                            }
                        }
                    }
                }
            }
        }
    }
    end{
        return $testModelsList -join ","
    } 
}


<#
    .SYNOPSIS
        Imports a .NET dll file into memory
         
    .DESCRIPTION
        Imports a .NET dll file into memory, by creating a copy (temporary file) and imports it using reflection
         
    .PARAMETER Path
        Path to the dll file you want to import
         
        Accepts an array of strings
         
    .PARAMETER UseTempFolder
        Instruct the cmdlet to create the file copy in the default temp folder
         
        This switch can be used, if writing to the original folder is not wanted or not possible
         
    .EXAMPLE
        PS C:\> Import-AssemblyFileIntoMemory -Path "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll"
         
        This will create an new file named "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll_shawdow.dll"
        The new file is then imported into memory using .NET Reflection.
        After the file has been imported, it will be deleted from disk.
         
    .EXAMPLE
        PS C:\> Import-AssemblyFileIntoMemory -Path "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll" -UseTempFolder
         
        This will create an new file named "Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll_shawdow.dll" in the temp folder
        The new file is then imported into memory using .NET Reflection.
        After the file has been imported, it will be deleted from disk.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Import-AssemblyFileIntoMemory {
    [CmdletBinding()]
    [OutputType()]
    param (
        [Parameter(Mandatory = $true, Position = 1)]
        [string[]] $Path,

        [switch] $UseTempFolder
    )

    if (-not (Test-PathExists -Path $Path -Type Leaf)) {
        Stop-PSFFunction -Message "Stopping because unable to locate file." -StepsUpward 1
        return
    }

    foreach ($itemPath in $Path) {

        if ($UseTempFolder) {
            $filename = Split-Path -Path $itemPath -Leaf
            $shadowClonePath = Join-Path $env:TEMP "$filename`_shadow.dll"
        }
        else {
            $shadowClonePath = "$itemPath`_shadow.dll"
        }

        try {
            Write-PSFMessage -Level Debug -Message "Cloning $itemPath to $shadowClonePath"
            Copy-Item -Path $itemPath -Destination $shadowClonePath -Force
    
            Write-PSFMessage -Level Debug -Message "Loading $shadowClonePath into memory"
            $null = [AppDomain]::CurrentDomain.Load(([System.IO.File]::ReadAllBytes($shadowClonePath)))
        }
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors"
            return
        }
        finally {
            Write-PSFMessage -Level Debug -Message "Removing $shadowClonePath"
            Remove-Item -Path $shadowClonePath -Force -ErrorAction SilentlyContinue
        }
    }
}


<#
    .SYNOPSIS
        Init the Azure Storage config variables
         
    .DESCRIPTION
        Update the active Azure Storage config variables that the module will use as default values
         
    .EXAMPLE
        PS C:\> Init-AzureStorageDefault
         
        This will update the Azure Storage variables.
         
    .NOTES
        This initializes the default NugetStorage settings
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Init-AzureStorageDefault {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
    [CmdletBinding()]
    [OutputType()]
    param ( )

    Register-FSCPSAzureStorageConfig -ConfigStorageLocation "System"
    
    Add-FSCPSAzureStorageConfig -Name NuGetStorage -SAS $Script:NuGetStorageSASToken -AccountId $Script:NuGetStorageAccountName -Container $Script:NuGetStorageContainer -Force
    Add-FSCPSAzureStorageConfig -Name ModelStorage -SAS $Script:ModelCacheStorageSASToken -AccountId $Script:NuGetStorageAccountName -Container $Script:ModelsStorageContainer -Force
}


<#
    .SYNOPSIS
        Invoke the D365Commerce compilation
         
    .DESCRIPTION
        Invoke the D365Commerce compilation
         
    .PARAMETER Version
        The version of the D365Commerce used to build
         
    .PARAMETER SourcesPath
        The folder contains a metadata files with binaries
         
    .PARAMETER BuildFolderPath
        The destination build folder
         
    .PARAMETER Force
        Cleanup destination build folder befor build
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSCompile -Version 10.0.39
         
        Example output:
         
        BUILD_FOLDER_PATH : c:\temp\fscps.tools\_bld\10.0.39_build
        BUILD_LOG_FILE_PATH : C:\Users\Administrator\AppData\Local\Temp\ScaleUnit.sln.msbuild.log
        CSU_ZIP_PATH : c:\temp\fscps.tools\_bld\artifacts\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip
        HW_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe
        SC_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe
        SU_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe
        PACKAGE_NAME : Master-ContosoForD365Commerce-10.0.39_20240530.48
        ARTIFACTS_PATH : c:\temp\fscps.tools\_bld\artifacts
        ARTIFACTS_LIST : ["C:\\temp\\fscps.tools\\_bld\\artifacts\\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\POS.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.Commerce.Runtime.Master-ContosoForD365Commerce-10.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoAddressWebService.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoWebService.2.2.63.1.nupkg"]
         
        This will build D365FSC package with version "10.0.39" to the Temp folder
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSCompile -SourcesPath "D:\Sources\connector-d365-commerce\"
         
        Example output:
         
        BUILD_FOLDER_PATH : c:\temp\fscps.tools\_bld\10.0.39_build
        BUILD_LOG_FILE_PATH : C:\Users\Administrator\AppData\Local\Temp\ScaleUnit.sln.msbuild.log
        CSU_ZIP_PATH : c:\temp\fscps.tools\_bld\artifacts\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip
        HW_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe
        SC_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe
        SU_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe
        PACKAGE_NAME : Master-ContosoForD365Commerce-10.0.39_20240530.48
        ARTIFACTS_PATH : c:\temp\fscps.tools\_bld\artifacts
        ARTIFACTS_LIST : ["C:\\temp\\fscps.tools\\_bld\\artifacts\\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\POS.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.Commerce.Runtime.Master-ContosoForD365Commerce-10.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoAddressWebService.2.2.63.1.nupkg",
        "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoWebService.2.2.63.1.nupkg"]
         
        This will build D365FSC package with version "10.0.39" to the Temp folder
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Invoke-CommerceCompile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param (
        [string] $Version,
        [Parameter(Mandatory = $true)]
        [string] $SourcesPath,
        [string] $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld),
        [switch] $Force
    )

    BEGIN {
        Invoke-TimeSignal -Start
        $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve
        . ($helperPath)        
        try{
            $CMDOUT = @{
                Verbose = If ($PSBoundParameters.Verbose -eq $true) { $true } else { $false };
                Debug = If ($PSBoundParameters.Debug -eq $true) { $true } else { $false }
            }
            $responseObject = [Ordered]@{}
            Write-PSFMessage -Level Important -Message "//================= Reading current FSC-PS settings ============================//"
            $settings = Get-FSCPSSettings @CMDOUT
            Write-PSFMessage -Level Important -Message "Complete"
            #if($Force)
            #{
                Write-PSFMessage -Level Important -Message "//================= Cleanup build folder =======================================//"
                Remove-Item $BuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue
                Write-PSFMessage -Level Important -Message "Complete"
            #}

            if($settings.artifactsPath -eq "")
            {
                $artifactDirectory = (Join-Path $BuildFolderPath $settings.artifactsFolderName)
            }
            else {
                $artifactDirectory = $settings.artifactsPath
            }

            if (Test-Path -Path $artifactDirectory -ErrorAction SilentlyContinue)
            {
                Remove-Item -Path $artifactDirectory -Recurse -Force -ErrorAction SilentlyContinue
                
            }

            if (!(Test-Path -Path $artifactDirectory))
            {
                $null = [System.IO.Directory]::CreateDirectory($artifactDirectory)
            }
            Get-ChildItem $artifactDirectory -Recurse
            if($Version -eq "")
            {
                $Version = $settings.buildVersion
            }

            if($Version -eq "")
            {
                throw "D365FSC Version should be specified."
            }            

            # Gather version info
            #$versionData = Get-FSCPSVersionInfo -Version $Version @CMDOUT

            $SolutionBuildFolderPath = (Join-Path $BuildFolderPath "$($Version)_build")
            $responseObject.BUILD_FOLDER_PATH = $SolutionBuildFolderPath
        }
        catch {
            Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
        finally{
            
        }
    }
    
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        try {               
            Write-PSFMessage -Level Important -Message "//================= Copy source files to the build folder ======================//"            
            $null = Test-PathExists -Path $BuildFolderPath -Type Container -Create @CMDOUT
            $null = Test-PathExists -Path $SolutionBuildFolderPath -Type Container -Create @CMDOUT
            Copy-Item $SourcesPath\* -Destination $SolutionBuildFolderPath -Recurse -Force @CMDOUT
            Write-PSFMessage -Level Important -Message "Complete"

            Write-PSFMessage -Level Important -Message "//================= Build solution =============================================//"

            $msbuildpath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products * -requires Microsoft.Component.MSBuild -property installationPath -latest

            $origLocation = Get-Location
            Set-Location $SolutionBuildFolderPath

            if($msbuildpath -ne "")
            {
                $msbuildexepath = Join-Path $msbuildpath "MSBuild\Current\Bin\MSBuild.exe"
                $msbuildresult = Invoke-MsBuild -Path (Join-Path $SolutionBuildFolderPath $settings.solutionName) -MsBuildParameters "/t:restore,rebuild /property:Configuration=Release /property:NuGetInteractive=true /property:BuildingInsideVisualStudio=false" -MsBuildFilePath "$msbuildexepath" -ShowBuildOutputInCurrentWindow -BypassVisualStudioDeveloperCommandPrompt @CMDOUT
            }
            else
            {
                $msbuildresult = Invoke-MsBuild -Path (Join-Path $SolutionBuildFolderPath $settings.solutionName) -MsBuildParameters "/t:restore,rebuild /property:Configuration=Release /property:NuGetInteractive=true /property:BuildingInsideVisualStudio=false" -ShowBuildOutputInCurrentWindow @CMDOUT
            }

            $responseObject.BUILD_LOG_FILE_PATH = $msbuildresult.BuildLogFilePath

            if ($msbuildresult.BuildSucceeded -eq $true)
            {
                Write-PSFMessage -Level Host -Message ("Build completed successfully in {0:N1} seconds." -f $msbuildresult.BuildDuration.TotalSeconds)
            }
            elseif ($msbuildresult.BuildSucceeded -eq $false)
            {
               throw ("Build failed after {0:N1} seconds. Check the build log file '$($msbuildresult.BuildLogFilePath)' for errors." -f $msbuildresult.BuildDuration.TotalSeconds)
            }
            elseif ($null -eq $msbuildresult.BuildSucceeded)
            {
                throw "Unsure if build passed or failed: $($msbuildresult.Message)"
            }
            Set-Location $origLocation
            if($settings.generatePackages)
            {
                Write-PSFMessage -Level Important -Message "//================= Generate package ==========================================//"

                switch ($settings.namingStrategy) {
                    { $settings.namingStrategy -eq "Default" }
                    {
                        $packageNamePattern = $settings.packageNamePattern;
                        if($settings.packageName.Contains('.zip'))
                        {
                            $packageName = $settings.packageName
                        }
                        else {
                            $packageName = $settings.packageName
                        }
                        $packageNamePattern = $packageNamePattern.Replace("BRANCHNAME", $($settings.sourceBranch))
                        if($settings.deploy)
                        {
                            $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $settings.azVMName)
                        }
                        else
                        {
                            $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $packageName)
                        }
                        $packageNamePattern = $packageNamePattern.Replace("FNSCMVERSION", $Version)
                        $packageNamePattern = $packageNamePattern.Replace("DATE", (Get-Date -Format "yyyyMMdd").ToString())
                        
                        $packageNamePattern = $packageNamePattern.Replace("RUNNUMBER", $settings.runId)

                        $packageName = $packageNamePattern
                        break;
                    }
                    { $settings.namingStrategy -eq "Custom" }
                    {
                        if($settings.packageName.Contains('.zip'))
                        {
                            $packageName = $settings.packageName
                        }
                        else {
                            $packageName = $settings.packageName + ".zip"
                        }
                        
                        break;
                    }
                    Default {
                        $packageName = $settings.packageName
                        break;
                    }
                }             
                

                [System.IO.DirectoryInfo]$csuZipPackagePath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*ScaleUnit.*.zip$"} | ForEach-Object {$_.FullName}
                [System.IO.DirectoryInfo]$hWSInstallerPath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*HardwareStation.*.exe$"} | ForEach-Object {$_.FullName}
                [System.IO.DirectoryInfo]$sCInstallerPath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*StoreCommerce.*.exe$"} | ForEach-Object {$_.FullName}
                [System.IO.DirectoryInfo]$sUInstallerPath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*ScaleUnit.*.exe$"} | ForEach-Object {$_.FullName}
                
                Write-PSFMessage -Level Important -Message "//================= Copy packages to the artifacts folder ======================//"
                if($csuZipPackagePath)
                {    
                    Write-PSFMessage -Level Important -Message "CSU Package processing..."
                    Write-PSFMessage -Level Important -Message $csuZipPackagePath
                    if($settings.cleanupCSUPackage)
                    {
                        $null = [Reflection.Assembly]::LoadWithPartialName('System.IO.Compression')
                        $zipfile = $csuZipPackagePath
                        $stream = New-Object IO.FileStream($zipfile, [IO.FileMode]::Open)
                        $mode   = [IO.Compression.ZipArchiveMode]::Update
                        $zip    = New-Object IO.Compression.ZipArchive($stream, $mode)
                        ($zip.Entries | Where-Object { $_.Name -match 'Azure' }) | ForEach-Object { $_.Delete() }
                        ($zip.Entries | Where-Object { $_.Name -match 'Microsoft' }) | ForEach-Object { $_.Delete() }
                        ($zip.Entries | Where-Object { $_.Name -match 'System'  -and $_.Name -notmatch 'System.Runtime.Caching' -and $_.Name -notmatch 'System.ServiceModel.Http' -and $_.Name -notmatch 'System.ServiceModel.Primitives' -and $_.Name -notmatch 'System.Private.ServiceModel' -and $_.Name -notmatch 'System.Configuration.ConfigurationManager' -and $_.Name -notmatch 'System.Security.Cryptography.ProtectedData' -and $_.Name -notmatch 'System.Security.Permissions' -and $_.Name -notmatch 'System.Security.Cryptography.Xml' -and $_.Name -notmatch 'System.Security.Cryptography.Pkcs' }) | ForEach-Object { $_.Delete() }
                        ($zip.Entries | Where-Object { $_.Name -match 'Newtonsoft' }) | ForEach-Object { $_.Delete() }
                        $zip.Dispose()
                        $stream.Close()
                        $stream.Dispose()
                    }
                    $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($csuZipPackagePath)).$($packageName).zip")
                    Copy-ToDestination -RelativePath $csuZipPackagePath.Parent.FullName -File $csuZipPackagePath.BaseName -DestinationFullName $destinationFullName
                    $responseObject.CSU_ZIP_PATH = $destinationFullName
                }
                if($hWSInstallerPath)
                {    
                    Write-PSFMessage -Level Important -Message "HW Package processing..."
                    Write-PSFMessage -Level Important -Message $hWSInstallerPath
                    $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($hWSInstallerPath)).$($packageName).exe")
                    Copy-ToDestination -RelativePath $hWSInstallerPath.Parent.FullName -File $hWSInstallerPath.BaseName -DestinationFullName $destinationFullName
                    $responseObject.HW_INSTALLER_PATH = $destinationFullName
                }
                if($sCInstallerPath)
                {    
                    Write-PSFMessage -Level Important -Message "SC Package processing..."
                    Write-PSFMessage -Level Important -Message $sCInstallerPath
                    $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($sCInstallerPath)).$($packageName).exe")
                    Copy-ToDestination -RelativePath $sCInstallerPath.Parent.FullName -File $sCInstallerPath.BaseName -DestinationFullName $destinationFullName
                    $responseObject.SC_INSTALLER_PATH = $destinationFullName
                }
                if($sUInstallerPath)
                {    
                    Write-PSFMessage -Level Important -Message "SU Package processing..."
                    Write-PSFMessage -Level Important -Message $sUInstallerPath
                    $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($sUInstallerPath)).$($packageName).exe")
                    Copy-ToDestination -RelativePath $sUInstallerPath.Parent.FullName -File $sUInstallerPath.BaseName -DestinationFullName $destinationFullName
                    $responseObject.SU_INSTALLER_PATH = $destinationFullName
                }

                Write-PSFMessage -Level Important -Message "//================= Export NuGets ===============================================//"
                Get-ChildItem -Path $BuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*.nupkg$"} | ForEach-Object {
                    if($settings.cleanupNugets)
                    {                
                        $zipfile = $_
                        # Cleanup NuGet file
                        $null = [Reflection.Assembly]::LoadWithPartialName('System.IO.Compression')            
                        $stream = New-Object IO.FileStream($zipfile.FullName, [IO.FileMode]::Open)
                        $mode   = [IO.Compression.ZipArchiveMode]::Update
                        $zip    = New-Object IO.Compression.ZipArchive($stream, $mode)
                        ($zip.Entries | Where-Object { $_.Name -match 'Azure' }) | ForEach-Object { $_.Delete() }
                        ($zip.Entries | Where-Object { $_.Name -match 'Microsoft' }) | ForEach-Object { $_.Delete() }
                        ($zip.Entries | Where-Object { $_.Name -match 'System' }) | ForEach-Object { $_.Delete() }
                        ($zip.Entries | Where-Object { $_.Name -match 'Newtonsoft' }) | ForEach-Object { $_.Delete() }
                        $zip.Dispose()
                        $stream.Close()
                        $stream.Dispose()
                    }
                    Copy-ToDestination -RelativePath $_.Directory -File $_.Name -DestinationFullName "$($artifactDirectory)\$($_.BaseName).nupkg"        
                }

                $responseObject.PACKAGE_NAME = $packageName
                $responseObject.ARTIFACTS_PATH = $artifactDirectory


                $artifacts = Get-ChildItem $artifactDirectory
                $artifactsList = $artifacts.FullName -join ","

                if($artifactsList.Contains(','))
                {
                    $artifacts = $artifactsList.Split(',') | ConvertTo-Json -compress
                }
                else
                {
                    $artifacts = '["'+$($artifactsList).ToString()+'"]'

                }

                $responseObject.ARTIFACTS_LIST = $artifacts
                        
            }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
        finally{
            try {
                if($SolutionBuildFolderPath)
                {
                    if (Test-Path -Path $SolutionBuildFolderPath -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $SolutionBuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
                if($NuGetPackagesPath)
                {
                    if (Test-Path -Path $NuGetPackagesPath -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $NuGetPackagesPath -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
                if($outputDir)
                {
                    if (Test-Path -Path $outputDir -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $outputDir -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
                if($tempCombinedPackage)
                {
                    if (Test-Path -Path $tempCombinedPackage -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $tempCombinedPackage -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
                Set-Location $origLocation
            }
            catch {
                Write-PSFMessage -Level Verbose -Message "Cleanup warning: $($PSItem.Exception)" 
            }
            $responseObject
        }
    }
    END {
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        This will import D365FSC base assemblies
         
    .DESCRIPTION
        This will import D365FSC base assemblies. For package generating process
         
    .PARAMETER binDir
        XppTools directory path
         
    .EXAMPLE
        PS C:\> Invoke-FSCAssembliesImport -DefaultRoot "C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp.7.0.7120.99\ref\net40"
         
    .NOTES
        General notes
#>

function Invoke-FSCAssembliesImport([string]$binDir)
{
    Write-PSFMessage -Level Verbose -Message "Importing metadata assemblies"

    # Need load metadata.dll and any referenced ones, not flexible to pick the new added references
    $m_core = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Core.dll
    $m_metadata = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.dll
    $m_storage = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Storage.dll
    $m_xppinstrumentation = Join-Path $binDir Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll
    $m_management_core = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Core.dll
    $m_management_delta = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Delta.dll
    $m_management_diff = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Diff.dll
    $m_management_merge = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Merge.dll

    # Load required dlls, loading should fail the script run with exceptions thrown
    [Reflection.Assembly]::LoadFile($m_core) > $null
    [Reflection.Assembly]::LoadFile($m_metadata) > $null
    [Reflection.Assembly]::LoadFile($m_storage) > $null
    [Reflection.Assembly]::LoadFile($m_xppinstrumentation) > $null
    [Reflection.Assembly]::LoadFile($m_management_core) > $null
    [Reflection.Assembly]::LoadFile($m_management_delta) > $null
    [Reflection.Assembly]::LoadFile($m_management_diff) > $null
    [Reflection.Assembly]::LoadFile($m_management_merge) > $null
}


<#
    .SYNOPSIS
        Invoke the D365FSC models compilation
         
    .DESCRIPTION
        Invoke the D365FSC models compilation
         
    .PARAMETER Version
        The version of the D365FSC used to build
         
    .PARAMETER SourcesPath
        The folder contains a metadata files with binaries
         
    .PARAMETER BuildFolderPath
        The destination build folder
         
    .PARAMETER Force
        Cleanup destination build folder befor build
         
    .EXAMPLE
        PS C:\> Invoke-FSCCompile -Version "10.0.39"
         
        Example output:
         
        METADATA_DIRECTORY : D:\a\8\s\Metadata
        FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99
        BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin
        NUGETS_FOLDER : C:\temp\buildbuild\packages
        BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log
        PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78
        PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip
        ARTIFACTS_PATH : C:\temp\buildbuild\artifacts
        ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"]
         
        This will build D365FSC package with version "10.0.39" to the Temp folder
         
    .EXAMPLE
        PS C:\> Invoke-FSCCompile -Version "10.0.39" -Path "c:\Temp"
         
        Example output:
         
        METADATA_DIRECTORY : D:\a\8\s\Metadata
        FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99
        BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin
        NUGETS_FOLDER : C:\temp\buildbuild\packages
        BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log
        PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78
        PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip
        ARTIFACTS_PATH : C:\temp\buildbuild\artifacts
        ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"]
         
        This will build D365FSC package with version "10.0.39" to the Temp folder
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Invoke-FSCCompile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param (
        [string] $Version,
        [Parameter(Mandatory = $true)]
        [string] $SourcesPath,
        [string] $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld),
        [switch] $Force
    )

    BEGIN {
        Invoke-TimeSignal -Start
        try{
            $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve
            . ($helperPath)
                    
            $CMDOUT = @{
                Verbose = If ($PSBoundParameters.Verbose -eq $true) { $true } else { $false };
                Debug = If ($PSBoundParameters.Debug -eq $true) { $true } else { $false }
            }
            $responseObject = [Ordered]@{}
            Write-PSFMessage -Level Important -Message "//================= Reading current FSC-PS settings ============================//"
            Write-PSFMessage -Level Important -Message "IsOneBox: $($Script:IsOnebox)"
            if($Script:IsOnebox)
            {
                Write-PSFMessage -Level Important -Message "EnvironmentType: $($Script:EnvironmentType)"
                Write-PSFMessage -Level Important -Message "HostName: $($environment.Infrastructure.HostName)"
                Write-PSFMessage -Level Important -Message "AOSPath: $($Script:AOSPath)"
                Write-PSFMessage -Level Important -Message "DatabaseServer: $($Script:DatabaseServer)"
                Write-PSFMessage -Level Important -Message "PackageDirectory: $($Script:PackageDirectory)"
                Write-PSFMessage -Level Important -Message "BinDirTools: $($Script:BinDirTools)"
                Write-PSFMessage -Level Important -Message "MetaDataDir: $($Script:MetaDataDir)"
            }
            $settings = Get-FSCPSSettings @CMDOUT

            if([string]::IsNullOrEmpty($Version))
            {
                $Version = $settings.buildVersion
            }

            if([string]::IsNullOrEmpty($Version))
            {
                throw "D365FSC Version should be specified."
            }

            if([string]::IsNullOrEmpty($BuildFolderPath))
            {
                $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld)
            }

            if([string]::IsNullOrEmpty($settings.sourceBranch))
            {
                $settings.sourceBranch = $settings.currentBranch
            }

            if([string]::IsNullOrEmpty($settings.artifactsPath))
            {
                $artifactDirectory = (Join-Path $BuildFolderPath $settings.artifactsFolderName)
            }            
            else {
                $artifactDirectory = $settings.artifactsPath
            }

            if (Test-Path -Path $artifactDirectory)
            {
                Remove-Item -Path $artifactDirectory -Recurse -Force
                $null = [System.IO.Directory]::CreateDirectory($artifactDirectory)
            }

            $buildLogsDirectory = (Join-Path $artifactDirectory "Logs")
            if (Test-Path -Path $buildLogsDirectory)
            {
                Remove-Item -Path $buildLogsDirectory -Recurse -Force
                $null = [System.IO.Directory]::CreateDirectory($buildLogsDirectory)
            }

            # Gather version info
            $versionData = Get-FSCPSVersionInfo -Version $Version @CMDOUT
            $PlatformVersion = $versionData.data.PlatformVersion
            $ApplicationVersion = $versionData.data.AppVersion

            $tools_package_name =  'Microsoft.Dynamics.AX.Platform.CompilerPackage.' + $PlatformVersion
            $plat_package_name =  'Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp.' + $PlatformVersion
            $app_package_name =  'Microsoft.Dynamics.AX.Application.DevALM.BuildXpp.' + $ApplicationVersion
            $appsuite_package_name =  'Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp.' + $ApplicationVersion 

            $NuGetPackagesPath = (Join-Path $BuildFolderPath packages)
            $SolutionBuildFolderPath = (Join-Path $BuildFolderPath "$($Version)_build")
            $NuGetPackagesConfigFilePath = (Join-Path $SolutionBuildFolderPath packages.config)
            $NuGetConfigFilePath = (Join-Path $SolutionBuildFolderPath nuget.config)
            
            if(Test-Path "$($SourcesPath)/PackagesLocalDirectory")
            {
                $SourceMetadataPath = (Join-Path $($SourcesPath) "/PackagesLocalDirectory")
            }
            elseif(Test-Path "$($SourcesPath)/Metadata")
            {
                $SourceMetadataPath = (Join-Path $($SourcesPath) "/Metadata")
            }
            else {
                $SourceMetadataPath = $($SourcesPath)
            }
            
            $BuidPropsFile = (Join-Path $SolutionBuildFolderPath \Build\build.props)
            
            $msReferenceFolder = "$($NuGetPackagesPath)\$($app_package_name)\ref\net40;$($NuGetPackagesPath)\$($plat_package_name)\ref\net40;$($NuGetPackagesPath)\$($appsuite_package_name)\ref\net40;$($SourceMetadataPath);$($BuildFolderPath)\bin"
            $msBuildTasksDirectory = "$NuGetPackagesPath\$($tools_package_name)\DevAlm".Trim()
            $msMetadataDirectory = "$($SourceMetadataPath)".Trim()
            $msFrameworkDirectory = "$($NuGetPackagesPath)\$($tools_package_name)".Trim()
            $msReferencePath = "$($NuGetPackagesPath)\$($tools_package_name)".Trim()
            $msOutputDirectory = "$($BuildFolderPath)\bin".Trim()     

            $responseObject.METADATA_DIRECTORY = $msMetadataDirectory
            $responseObject.FRAMEWORK_DIRECTORY = $msFrameworkDirectory
            $responseObject.BUILD_OUTPUT_DIRECTORY = $msOutputDirectory
            $responseObject.BUILD_FOLDER_PATH = $BuildFolderPath

            Write-PSFMessage -Level Important -Message "//================= Getting the list of models to build ========================//"
            if($($settings.specifyModelsManually) -eq "true")
            {
                $mtdtdPath = ("$($SourcesPath)\$($settings.metadataPath)".Trim())
                $mdls = $($settings.models).Split(",")
                if($($settings.includeTestModel) -eq "true")
                {
                    $testModels = Get-FSCMTestModel -modelNames $($mdls -join ",") -metadataPath $mtdtdPath @CMDOUT
                    ($testModels.Split(",").ForEach({$mdls+=($_)}))
                }
                $models = $mdls -join ","
                $modelsToPackage = $models
            }
            else {
                $models = Get-FSCModelList -MetadataPath $SourceMetadataPath -IncludeTest:($settings.includeTestModel -eq 'true') @CMDOUT         
                
                if($settings.enableBuildCaching)
                {
                    Write-PSFMessage -Level Important -Message "Model caching is enabled."
                    if(($settings.repoProvider -eq "GitHub") -or ($settings.repoProvider -eq "AzureDevOps"))
                    {
                        $modelsHash = [Ordered]@{}
                        $modelsToCache = @()
                        Write-PSFMessage -Level Important -Message "Running in $($settings.repoProvider). Start processing"

                        foreach ($model in $models.Split(","))
                        {                            
                            $modelName = $model
                            Write-PSFMessage -Level Important -Message "Model: $modelName cache validation"
                            $modelRootPath = (Join-Path $SourceMetadataPath $modelName )
                            $modelHash = Get-FolderHash $modelRootPath
                            $modelsHash.$modelName = $modelHash

                            $validation = Validate-FSCModelCache -MetadataDirectory $SourceMetadataPath -RepoOwner $settings.repoOwner -RepoName $settings.repoName -ModelName $modelName -Version $Version -BranchName $settings.sourceBranch
                            if(-not $validation)
                            {
                                $modelsToCache += ($modelName)
                            }
                        }
                        if($modelsToCache)
                        {
                            $modelsToBuild = $modelsToCache -join ","
                        }
                    }
                    else {
                        $modelsToBuild = $models
                    }
                }
                else {
                    $modelsToBuild = $models
                }
                $modelsToPackage = Get-FSCModelList -MetadataPath $SourceMetadataPath -IncludeTest:($settings.includeTestModel -eq 'true') -All @CMDOUT    
            }
            if(-not $modelsToBuild){$modelsToBuild = ""}
            Write-PSFMessage -Level Important -Message "Models to build: $modelsToBuild"
            Write-PSFMessage -Level Important -Message "Models to package: $modelsToPackage"
        }
        catch {
            Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
        finally{
            
        }
    }
    
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        try {            
            if($Force)
            {
                Write-PSFMessage -Level Important -Message "//================= Cleanup build folder =======================================//"
                Remove-Item $BuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue
            }

            Write-PSFMessage -Level Important -Message "//================= Generate solution folder ===================================//"
            $null = Invoke-GenerateSolution -ModelsList $modelsToBuild -Version "$Version" -MetadataPath $SourceMetadataPath -SolutionFolderPath $BuildFolderPath @CMDOUT
            Write-PSFMessage -Level Important -Message "Complete"

            Write-PSFMessage -Level Important -Message "//================= Copy source files to the build folder ======================//"            
            $null = Test-PathExists -Path $BuildFolderPath -Type Container -Create @CMDOUT
            $null = Test-PathExists -Path $SolutionBuildFolderPath -Type Container -Create @CMDOUT
            Write-PSFMessage -Level Important -Message "Source folder: $SourcesPath"
            Write-PSFMessage -Level Important -Message "Destination folder: $BuildFolderPath"
            Copy-Item $SourcesPath\* -Destination $BuildFolderPath -Recurse -Force @CMDOUT
            Write-PSFMessage -Level Important -Message "Complete"
    
            Write-PSFMessage -Level Important -Message "//================= Download NuGet packages ====================================//"
            $null = Test-PathExists -Path $NuGetPackagesPath -Type Container -Create @CMDOUT
            $null = Get-FSCPSNuget -Version $PlatformVersion -Type PlatformCompilerPackage -Path $NuGetPackagesPath -Force @CMDOUT
            $null = Get-FSCPSNuget -Version $PlatformVersion -Type PlatformDevALM -Path $NuGetPackagesPath -Force @CMDOUT
            $null = Get-FSCPSNuget -Version $ApplicationVersion -Type ApplicationDevALM -Path $NuGetPackagesPath -Force @CMDOUT
            $null = Get-FSCPSNuget -Version $ApplicationVersion -Type ApplicationSuiteDevALM -Path $NuGetPackagesPath -Force @CMDOUT

            Write-PSFMessage -Level Important -Message "Complete"
            $responseObject.NUGETS_FOLDER = $NuGetPackagesPath
            
            Write-PSFMessage -Level Important -Message "//================= Install NuGet packages =====================================//"
            #validata NuGet installation
            $nugetPath = Get-PSFConfigValue -FullName "fscps.tools.path.nuget"
            if(-not (Test-Path $nugetPath))
            {
                Install-FSCPSNugetCLI
            }
            ##update nuget config file
            $nugetNewContent = (Get-Content $NuGetConfigFilePath).Replace('c:\temp\packages', $NuGetPackagesPath)
            Set-Content $NuGetConfigFilePath $nugetNewContent
                
            $null = (& $nugetPath restore $NuGetPackagesConfigFilePath -PackagesDirectory $NuGetPackagesPath -ConfigFile $NuGetConfigFilePath)
            Write-PSFMessage -Level Important -Message "Complete"

            Write-PSFMessage -Level Important -Message "//================= Copy binaries to the build folder ==========================//"
            Copy-Filtered -Source $SourceMetadataPath -Target (Join-Path $BuildFolderPath bin) -Filter *.*
            Write-PSFMessage -Level Important -Message "Complete"

            if($modelsToBuild)
            {
                Write-PSFMessage -Level Important -Message "//================= Build solution =============================================//"
                
                Set-Content $BuidPropsFile (Get-Content $BuidPropsFile).Replace('ReferenceFolders', $msReferenceFolder)

                $msbuildresult = Invoke-MsBuild -Path (Join-Path $SolutionBuildFolderPath "\Build\Build.sln") -P "/p:BuildTasksDirectory=$msBuildTasksDirectory /p:MetadataDirectory=$msMetadataDirectory /p:FrameworkDirectory=$msFrameworkDirectory /p:ReferencePath=$msReferencePath /p:OutputDirectory=$msOutputDirectory" -ShowBuildOutputInCurrentWindow @CMDOUT

                $responseObject.BUILD_LOG_FILE_PATH = $msbuildresult.BuildLogFilePath

                Copy-Filtered -Source (Join-Path $SolutionBuildFolderPath "Build") -Target $buildLogsDirectory -Filter *Dynamics.AX.*.xppc.*
                Copy-Filtered -Source (Join-Path $SolutionBuildFolderPath "Build") -Target $buildLogsDirectory -Filter *Dynamics.AX.*.labelc.*
                Copy-Filtered -Source (Join-Path $SolutionBuildFolderPath "Build") -Target $buildLogsDirectory -Filter *Dynamics.AX.*.reportsc.*

                Get-ChildItem -Path $buildLogsDirectory | ForEach-Object { if($_.Length -eq 0) {$_.Delete()}}

                
                if ($msbuildresult.BuildSucceeded -eq $true)
                {
                    Write-PSFMessage -Level Host -Message ("Build completed successfully in {0:N1} seconds." -f $msbuildresult.BuildDuration.TotalSeconds)
                    if($settings.enableBuildCaching)
                    {
                        Write-PSFMessage -Level Important -Message "//================= Upload cached models to the storageaccount ================//"
                        foreach ($model in $modelsToBuild.Split(","))
                        {
                            $modelName = $model
                            $modelHash = $modelsHash.$modelName
                            $modelBinPath = (Join-Path $msOutputDirectory $modelName)
                            $modelFileNameWithHash = "$(($settings.repoOwner).ToLower())_$(($settings.repoName).ToLower())_$($modelName.ToLower())_$($settings.sourceBranch.ToLower())_$($Version)_$($modelHash).7z".Replace(" ", "-")
                            $modelArchivePath = (Join-Path $BuildFolderPath $modelFileNameWithHash)

                            $storageConfigs = Get-FSCPSAzureStorageConfig
                            $activeStorageConfigName = "ModelStorage"
                            if($storageConfigs)
                            {
                                $activeStorageConfig = Get-FSCPSActiveAzureStorageConfig
                                $storageConfigs | ForEach-Object {
                                    if($_.AccountId -eq $activeStorageConfig.AccountId -and $_.Container -eq $activeStorageConfig.Container -and $_.SAS -eq $activeStorageConfig.SAS)
                                    {
                                        if($activeStorageConfigName)
                                        {
                                            $activeStorageConfigName = $_.Name
                                        }
                                    }
                                }
                            }
                            Write-PSFMessage -Level Host -Message "Uploading compiled model binaries: $modelName"
                            Write-PSFMessage -Level Host -Message "File: $modelFileNameWithHash"
                            Compress-7zipArchive -Path $modelBinPath\* -DestinationPath $modelArchivePath
                            Set-FSCPSActiveAzureStorageConfig ModelStorage
                            $null = Invoke-FSCPSAzureStorageUpload -FilePath $modelArchivePath
                            if(-not [string]::IsNullOrEmpty($activeStorageConfigName)){
                                Set-FSCPSActiveAzureStorageConfig $activeStorageConfigName
                            }

                        }
                        Write-PSFMessage -Level Important -Message "Complete"
                    }
                }
                elseif ($msbuildresult.BuildSucceeded -eq $false)
                {
                throw ("Build failed after {0:N1} seconds. Check the build log file '$($msbuildresult.BuildLogFilePath)' for errors." -f $msbuildresult.BuildDuration.TotalSeconds)
                }
                elseif ($null -eq $msbuildresult.BuildSucceeded)
                {
                    throw "Unsure if build passed or failed: $($msbuildresult.Message)"
                }
            }
            
            if($settings.generatePackages)
            {
                if($PSVersionTable.PSVersion.Major -gt 5) {
                    Write-PSFMessage -Level Warning -Message "Current PS version is $($PSVersionTable.PSVersion). The latest PS version acceptable to generate the D365FSC deployable package is 5."
                }
                else {                
                    Write-PSFMessage -Level Important -Message "//================= Generate package ==========================================//"

                    switch ($settings.namingStrategy) {
                        { $settings.namingStrategy -eq "Default" }
                        {
                            $packageNamePattern = $settings.packageNamePattern;
                            if($settings.packageName.Contains('.zip'))
                            {
                                $packageName = $settings.packageName
                            }
                            else {
                                $packageName = $settings.packageName# + ".zip"
                            }
                            $packageNamePattern = $packageNamePattern.Replace("BRANCHNAME", $($settings.sourceBranch))
                            if($settings.deploy)
                            {
                                $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $settings.azVMName)
                            }
                            else
                            {
                                $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $packageName)
                            }
                            $packageNamePattern = $packageNamePattern.Replace("FNSCMVERSION", $Version)
                            $packageNamePattern = $packageNamePattern.Replace("DATE", (Get-Date -Format "yyyyMMdd").ToString())                        
                            $packageNamePattern = $packageNamePattern.Replace("RUNNUMBER", $settings.runId)

                            $packageName = $packageNamePattern + ".zip"
                            break;
                        }
                        { $settings.namingStrategy -eq "Custom" }
                        {
                            if($settings.packageName.Contains('.zip'))
                            {
                                $packageName = $settings.packageName
                            }
                            else {
                                $packageName = $settings.packageName + ".zip"
                            }
                            
                            break;
                        }
                        Default {
                            $packageName = $settings.packageName
                            break;
                        }
                    }                

                    $xppToolsPath = $msFrameworkDirectory
                    $xppBinariesPath = (Join-Path $($BuildFolderPath) bin)
                    $xppBinariesSearch = $modelsToPackage
                    $deployablePackagePath = Join-Path $artifactDirectory ($packageName)

                    if ($xppBinariesSearch.Contains(","))
                    {
                        [string[]]$xppBinariesSearch = $xppBinariesSearch -split ","
                    }

                    $potentialPackages = Find-FSCPSMatch -DefaultRoot $xppBinariesPath -Pattern $xppBinariesSearch | Where-Object { (Test-Path -LiteralPath $_ -PathType Container) }
                    $packages = @()
                    if ($potentialPackages.Length -gt 0)
                    {
                        Write-PSFMessage -Level Verbose -Message "Found $($potentialPackages.Length) potential folders to include:"
                        foreach($package in $potentialPackages)
                        {
                            $packageBinPath = Join-Path -Path $package -ChildPath "bin"
                            
                            # If there is a bin folder and it contains *.MD files, assume it's a valid X++ binary
                            try {
                                if ((Test-Path -Path $packageBinPath) -and ((Get-ChildItem -Path $packageBinPath -Filter *.md).Count -gt 0))
                                {
                                    Write-PSFMessage -Level Verbose -Message $packageBinPath
                                    Write-PSFMessage -Level Verbose -Message " - $package"
                                    $packages += $package
                                }
                            }
                            catch
                            {
                                Write-PSFMessage -Level Verbose -Message " - $package (not an X++ binary folder, skip)"
                            }
                        }

                        Import-Module (Join-Path -Path $xppToolsPath -ChildPath "CreatePackage.psm1")
                        $outputDir = Join-Path -Path $BuildFolderPath -ChildPath ((New-Guid).ToString())
                        $tempCombinedPackage = Join-Path -Path $BuildFolderPath -ChildPath "$((New-Guid).ToString()).zip"
                        try
                        {
                            New-Item -Path $outputDir -ItemType Directory > $null
                            Write-PSFMessage -Level Verbose -Message "Creating binary packages"
                            Invoke-FSCAssembliesImport $xppToolsPath -Verbose
                            foreach($packagePath in $packages)
                            {
                                $packageName = (Get-Item $packagePath).Name
                                Write-PSFMessage -Level Verbose -Message " - '$packageName'"
                                $version = ""
                                $packageDll = Join-Path -Path $packagePath -ChildPath "bin\Dynamics.AX.$packageName.dll"
                                if (Test-Path $packageDll)
                                {
                                    $version = (Get-Item $packageDll).VersionInfo.FileVersion
                                }
                                if (!$version)
                                {
                                    $version = "1.0.0.0"
                                }
                                $null = New-XppRuntimePackage -packageName $packageName -packageDrop $packagePath -outputDir $outputDir -metadataDir $xppBinariesPath -packageVersion $version -binDir $xppToolsPath -enforceVersionCheck $True
                            }

                            Write-PSFMessage -Level Important "Creating deployable package"
                            Add-Type -Path "$xppToolsPath\Microsoft.Dynamics.AXCreateDeployablePackageBase.dll"
                            Write-PSFMessage -Level Important " - Creating combined metadata package"
                            $null = [Microsoft.Dynamics.AXCreateDeployablePackageBase.BuildDeployablePackages]::CreateMetadataPackage($outputDir, $tempCombinedPackage)
                            Write-PSFMessage -Level Important " - Creating merged deployable package"
                            $null = [Microsoft.Dynamics.AXCreateDeployablePackageBase.BuildDeployablePackages]::MergePackage("$xppToolsPath\BaseMetadataDeployablePackage.zip", $tempCombinedPackage, $deployablePackagePath, $true, [String]::Empty)
                            Write-PSFMessage -Level Important "Deployable package '$deployablePackagePath' successfully created."

                            $pname = ($deployablePackagePath.SubString("$deployablePackagePath".LastIndexOf('\') + 1)).Replace(".zip","")
                            $responseObject.PACKAGE_NAME = $pname
                            $responseObject.PACKAGE_PATH = $deployablePackagePath
                            $responseObject.ARTIFACTS_PATH = $artifactDirectory
                        }
                        catch {
                            throw $_.Exception.Message
                        }
                    }
                    else
                    {
                        throw "No X++ binary package(s) found"
                    }
                    Write-PSFMessage -Level Important -Message "Complete"
                }
            }
            if($settings.exportModel)
            {
                Write-PSFMessage -Level Important -Message "//================= Export models ===========================================//"

                try {
                    $axModelFolder = Join-Path $artifactDirectory AxModels
                    $null = Test-PathExists -Path $axModelFolder -Type Container -Create
                    Write-PSFMessage -Level Verbose -Message "$axModelFolder created"

                    if($models.Split(","))
                    {                                
                        $modelsList = $models.Split(",")
                        foreach ($currentModel in $modelsList) {
                            Write-PSFMessage -Level Verbose -Message "Exporting $currentModel model..."
                            $modelName = (Get-AXModelName -ModelName $currentModel -ModelPath $msMetadataDirectory)
                            if($modelName)
                            {
                                $modelFilePath = Export-D365Model -Path $axModelFolder -Model $modelName -BinDir $msFrameworkDirectory -MetaDataDir $msMetadataDirectory -ShowOriginalProgress 
                                $modelFile = Get-Item $modelFilePath.File
                                Rename-Item $modelFile.FullName (($currentModel)+($modelFile.Extension)) -Force
                            }
                            else
                            {
                                Write-PSFMessage -Level Verbose -Message "The model $modelName doesn`t have the source code. Skipped."
                            }
                        }
                    }
                    else {
                        Write-PSFMessage -Level Verbose -Message "Exporting $models model..."
                        $modelName = (Get-AXModelName -ModelName $models -ModelPath $msMetadataDirectory)
                        if($modelName)
                        {
                            $modelFilePath = Export-D365Model -Path $axModelFolder -Model $modelName -BinDir $msFrameworkDirectory -MetaDataDir $msMetadataDirectory
                            $modelFile = Get-Item $modelFilePath.File
                            Rename-Item $modelFile.FullName (($models)+($modelFile.Extension)) -Force
                        }
                        else
                        {
                            Write-PSFMessage -Level Verbose -Message "The model $models doesn`t have the source code. Skipped."
                        }
                    }
                }
                catch {
                    Write-PSFMessage -Level Important -Message $_.Exception.Message
                }
                Write-PSFMessage -Level Important -Message "Complete"
                
            }
            $artifacts = Get-ChildItem $artifactDirectory -File -Recurse
            $artifactsList = $artifacts.FullName -join ","

            if($artifactsList.Contains(','))
            {
                $artifacts = $artifactsList.Split(',') | ConvertTo-Json -compress
            }
            else
            {
                $artifacts = '["'+$($artifactsList).ToString()+'"]'
            }
            
            $responseObject.ARTIFACTS_LIST = $artifacts
        }
        catch {
            Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
        finally
        {
            try {
                if($SolutionBuildFolderPath)
                {
                    if (Test-Path -Path $SolutionBuildFolderPath -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $SolutionBuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
                if($NuGetPackagesPath)
                {
                    if (Test-Path -Path $NuGetPackagesPath -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $NuGetPackagesPath -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
                if($outputDir)
                {
                    if (Test-Path -Path $outputDir -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $outputDir -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
                if($tempCombinedPackage)
                {
                    if (Test-Path -Path $tempCombinedPackage -ErrorAction SilentlyContinue)
                    {
                        Remove-Item -Path $tempCombinedPackage -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
            }
            catch {
                Write-PSFMessage -Level Verbose -Message "Cleanup warning: $($PSItem.Exception)" 
            }            
            $responseObject
        }
    }
    END {
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        HTTP request wrapper
         
    .DESCRIPTION
        HTTP request wrapper
         
    .PARAMETER headers
        HTTP request headers parameter
         
    .PARAMETER method
        HTTP request method parameter
         
    .PARAMETER body
        HTTP request body parameter
         
    .PARAMETER outFile
        HTTP outfile parameter
         
    .PARAMETER uri
        Parameter description
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSWebRequest -Uri "google.com"
         
        This will invoke google.com
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Invoke-FSCPSWebRequest {
    Param(
        [Hashtable] $headers,
        [string] $method,
        [string] $body,
        [string] $outFile,
        [string] $uri
    )

    try {
        $params = @{ "UseBasicParsing" = $true }
        if ($headers) {
            $params += @{ "headers" = $headers }
        }
        if ($method) {
            $params += @{ "method" = $method }
        }
        if ($body) {
            $params += @{ "body" = $body }
        }
        if ($outfile) {
            if(-not (Test-Path $outFile))
            {
                $null = New-Item -Path $outFile -Force
            }
            
            $params += @{ "outfile" = $outfile }
        }
        Invoke-WebRequest  @params -Uri $uri
    }
    catch {
        $errorRecord = $_
        $exception = $_.Exception
        $message = $exception.Message
        try {
            if ($errorRecord.ErrorDetails) {
                $errorDetails = $errorRecord.ErrorDetails | ConvertFrom-Json 
                $errorDetails.psObject.Properties.name | ForEach-Object {
                    $message += " $($errorDetails."$_")"
                }
                
            }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Error occured"
        }
        throw $message
    }
}


<#
    .SYNOPSIS
        Generate the D365FSC build solution
         
    .DESCRIPTION
        Invoke the D365FSC generation build solution
         
    .PARAMETER ModelsList
        The list of models to generate a solution
         
    .PARAMETER DynamicsVersion
        The version of the D365FSC to build
         
    .PARAMETER MetadataPath
        The path to the metadata folder
         
    .PARAMETER SolutionBasePath
        The path to the generated solution folder. Dafault is c:\temp\fscps.tools\
         
    .EXAMPLE
        PS C:\> Invoke-GenerateSolution -Models "Test, SuperTest, SuperTestExtension" -Version "10.0.39" -MetadataPath "c:\temp\TestMetadataFolder"
         
        This will generate a solution of 10.0.39 version
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Invoke-GenerateSolution {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [Alias('Models')]
        [string]$ModelsList,
        [Parameter(Mandatory = $true)]
        [Alias('Version')]
        [string]$DynamicsVersion,
        [Parameter(Mandatory = $true)]
        [string]$MetadataPath,
        [Alias('SolutionFolderPath')]
        [string]$SolutionBasePath = $script:DefaultTempPath
    )


    BEGIN
    {
        $miscFolder = (Join-Path $script:ModuleRoot "\internal\misc")
        $buildSolutionTemplateFolder = (Join-Path $miscFolder \Build)
        $buildProjectTemplateFolder = (Join-Path $buildSolutionTemplateFolder \Build)

        #Set-Location $buildProjectTemplateFolder
        Write-PSFMessage -Level Debug -Message  "MetadataPath: $MetadataPath"

        $ProjectPattern = 'Project("{FC65038C-1B2F-41E1-A629-BED71D161FFF}") = "ModelNameBuild (ISV) [ModelDisplayName]", "ModelName.rnrproj", "{62C69717-A1B6-43B5-9E86-24806782FEC2}"'
        $ActiveCFGPattern = ' {62C69717-A1B6-43B5-9E86-24806782FEC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU'
        $BuildPattern = ' {62C69717-A1B6-43B5-9E86-24806782FEC2}.Debug|Any CPU.Build.0 = Debug|Any CPU'

        $SolutionFileName =  'Build.sln'
        $NugetFolderPath =  Join-Path $SolutionBasePath "$($DynamicsVersion)_build"
        $SolutionFolderPath = Join-Path  $NugetFolderPath 'Build'
        $NewSolutionName = Join-Path  $SolutionFolderPath 'Build.sln'

        function Get-AXModelDisplayName {
            param (
                [Alias('ModelName')]
                [string]$_modelName,
                [Alias('ModelPath')]
                [string]$_modelPath
            )
            process{
                $descriptorSearchPath = (Join-Path $_modelPath (Join-Path $_modelName "Descriptor"))
                $descriptor = (Get-ChildItem -Path $descriptorSearchPath -Filter '*.xml')
                if($descriptor)
                {
                    Write-PSFMessage -Level Verbose -Message "Descriptor found at $descriptor"
                    [xml]$xmlData = Get-Content $descriptor.FullName
                    $modelDisplayName = $xmlData.SelectNodes("//AxModelInfo/DisplayName")
                    return $modelDisplayName.InnerText
                }
            }
        }
        function GenerateProjectFile {
            [CmdletBinding()]
            param (
                [string]$ModelName,
                [string]$MetadataPath,
                [string]$ProjectGuid
            )
        
            $ProjectFileName =  'Build.rnrproj'
            $ModelProjectFileName = $ModelName + '.rnrproj'
            $NugetFolderPath =  Join-Path $SolutionBasePath "$($DynamicsVersion)_build"
            $SolutionFolderPath = Join-Path  $NugetFolderPath 'Build'
            $ModelProjectFile = Join-Path $SolutionFolderPath $ModelProjectFileName
            #$modelDisplayName = Get-AXModelDisplayName -ModelName $ModelName -ModelPath $MetadataPath
            $modelDescriptorName = Get-AXModelName -ModelName $ModelName -ModelPath $MetadataPath 
            #generate project file
        
            if($modelDescriptorName -eq "")
            {
                $ProjectFileData = (Get-Content $buildProjectTemplateFolder\$ProjectFileName).Replace('ModelName', $ModelName).Replace('62C69717-A1B6-43B5-9E86-24806782FEC2'.ToLower(), $ProjectGuid.ToLower())
            }
            else {
                $ProjectFileData = (Get-Content $buildProjectTemplateFolder\$ProjectFileName).Replace('ModelName', $modelDescriptorName).Replace('62C69717-A1B6-43B5-9E86-24806782FEC2'.ToLower(), $ProjectGuid.ToLower())
            }
            #$ProjectFileData = (Get-Content $ProjectFileName).Replace('ModelName', $modelDescriptorName).Replace('62C69717-A1B6-43B5-9E86-24806782FEC2'.ToLower(), $ProjectGuid.ToLower())
             
            Set-Content $ModelProjectFile $ProjectFileData
        }
        
    }

    PROCESS
    {    

        New-Item -ItemType Directory -Path $SolutionFolderPath -ErrorAction SilentlyContinue
        Copy-Item $buildProjectTemplateFolder\build.props -Destination $SolutionFolderPath -force

    
        [String[]] $SolutionFileData = @() 
    
        $projectGuids = @{};
        Write-PSFMessage -Level Debug -Message  "Generate projects GUIDs..."
        if($ModelsList)
        {
            Foreach($model in $ModelsList.Split(','))
            {
                $projectGuids.Add($model, ([string][guid]::NewGuid()).ToUpper())
            }
            Write-PSFMessage -Level Debug -Message $projectGuids

            #generate project files file
            $FileOriginal = Get-Content $buildProjectTemplateFolder\$SolutionFileName
                
            Write-PSFMessage -Level Debug -Message  "Parse files"
            Foreach ($Line in $FileOriginal)
            {   
                $SolutionFileData += $Line

                    Foreach($model in $ModelsList.Split(','))
                    {
                        $projectGuid = $projectGuids.Item($model)
            
                        if ($Line -eq $ProjectPattern) 
                        {
                            Write-PSFMessage -Level Debug -Message  "Get AXModel Display Name"
                            $modelDisplayName = Get-AXModelDisplayName -ModelName $model -ModelPath $MetadataPath 
                            Write-PSFMessage -Level Debug -Message  "AXModel Display Name is $modelDisplayName"
                            Write-PSFMessage -Level Debug -Message  "Update Project line"
                            $newLine = $ProjectPattern -replace 'ModelName', $model
                            $newLine = $newLine -replace 'ModelDisplayName', $modelDisplayName
                            $newLine = $newLine -replace 'Build.rnrproj', ($model+'.rnrproj')
                            $newLine = $newLine -replace '62C69717-A1B6-43B5-9E86-24806782FEC2', $projectGuid
                            #Add Lines after the selected pattern
                            $SolutionFileData += $newLine                
                            $SolutionFileData += "EndProject"
                    
                        } 
                        if ($Line -eq $ActiveCFGPattern) 
                        { 
                            Write-PSFMessage -Level Debug -Message  "Update Active CFG line"
                            $newLine = $ActiveCFGPattern -replace '62C69717-A1B6-43B5-9E86-24806782FEC2', $projectGuid
                            $SolutionFileData += $newLine
                        } 
                        if ($Line -eq $BuildPattern) 
                        {
                            Write-PSFMessage -Level Debug -Message  "Update Build line"
                            $newLine = $BuildPattern -replace '62C69717-A1B6-43B5-9E86-24806782FEC2', $projectGuid
                            $SolutionFileData += $newLine
                        } 
                    }
                

            }
            Write-PSFMessage -Level Debug -Message  "Save solution file"
            #save solution file
            Set-Content $NewSolutionName $SolutionFileData;
            #cleanup solution file
            $tempFile = Get-Content $NewSolutionName
            $tempFile | Where-Object {$_ -ne $ProjectPattern} | Where-Object {$_ -ne $ActiveCFGPattern} | Where-Object {$_ -ne $BuildPattern} | Set-Content -Path $NewSolutionName 
        
            #generate project files
            Foreach($project in $projectGuids.GetEnumerator())
            {
                GenerateProjectFile -ModelName $project.Name -ProjectGuid $project.Value -MetadataPath $MetadataPath 
            }
        
            #Set-Location $buildSolutionTemplateFolder
        }
        #generate nuget.config
        $NugetConfigFileName = 'nuget.config'
        $NewNugetFile = Join-Path $NugetFolderPath $NugetConfigFileName
        if($NugetFeedName)
        {
            $tempFile = (Get-Content $buildSolutionTemplateFolder\$NugetConfigFileName).Replace('NugetFeedName', $NugetFeedName).Replace('NugetSourcePath', $NugetSourcePath)
        }
        else {
            $tempFile = (Get-Content $buildSolutionTemplateFolder\$NugetConfigFileName).Replace('<add key="NugetFeedName" value="NugetSourcePath" />', '')
        }
        Set-Content $NewNugetFile $tempFile    
        $version = Get-FSCPSVersionInfo -Version "$DynamicsVersion"
        #generate packages.config
        $PackagesConfigFileName = 'packages.config'
        $NewPackagesFile = Join-Path $NugetFolderPath $PackagesConfigFileName
        $tempFile = (Get-Content $buildSolutionTemplateFolder\$PackagesConfigFileName).Replace('PlatformVersion', $version.data.PlatformVersion).Replace('ApplicationVersion', $version.data.AppVersion)
        Set-Content $NewPackagesFile $tempFile
    }   

    END{

    }
    
}


<#
    .SYNOPSIS
        Invoke the ModelUtil.exe
         
    .DESCRIPTION
        A cmdlet that wraps some of the cumbersome work into a streamlined process
         
    .PARAMETER Command
        Instruct the cmdlet to what process you want to execute against the ModelUtil tool
         
        Valid options:
        Import
        Export
        Delete
        Replace
         
    .PARAMETER Path
        Used for import to point where to import from
        Used for export to point where to export the model to
         
        The cmdlet only supports an already extracted ".axmodel" file
         
    .PARAMETER Model
        Name of the model that you want to work against
         
        Used for export to select the model that you want to export
        Used for delete to select the model that you want to delete
         
    .PARAMETER BinDir
        The path to the bin directory for the environment
         
        Default path is the same as the AOS service PackagesLocalDirectory\bin
         
        Default value is fetched from the current configuration on the machine
         
    .PARAMETER MetaDataDir
        The path to the meta data directory for the environment
         
        Default path is the same as the aos service PackagesLocalDirectory
         
    .PARAMETER LogPath
        The path where the log file(s) will be saved
         
    .PARAMETER ShowOriginalProgress
        Instruct the cmdlet to show the standard output in the console
         
        Default is $false which will silence the standard output
         
    .PARAMETER OutputCommandOnly
        Instruct the cmdlet to only output the command that you would have to execute by hand
         
        Will include full path to the executable and the needed parameters based on your selection
         
    .EXAMPLE
        PS C:\> Invoke-ModelUtil -Command Import -Path "c:\temp\d365fo.tools\CustomModel.axmodel"
         
        This will execute the import functionality of ModelUtil.exe and have it import the "CustomModel.axmodel" file.
         
    .EXAMPLE
        PS C:\> Invoke-ModelUtil -Command Export -Path "c:\temp\d365fo.tools" -Model CustomModel
         
        This will execute the export functionality of ModelUtil.exe and have it export the "CustomModel" model.
        The file will be placed in "c:\temp\d365fo.tools".
         
    .EXAMPLE
        PS C:\> Invoke-ModelUtil -Command Delete -Model CustomModel
         
        This will execute the delete functionality of ModelUtil.exe and have it delete the "CustomModel" model.
        The folders in PackagesLocalDirectory for the "CustomModel" will NOT be deleted
         
    .EXAMPLE
        PS C:\> Invoke-ModelUtil -Command Replace -Path "c:\temp\d365fo.tools\CustomModel.axmodel"
         
        This will execute the replace functionality of ModelUtil.exe and have it replace the "CustomModel" model.
         
    .NOTES
        Tags: AXModel, Model, ModelUtil, Servicing, Import, Export, Delete, Replace
        This is refactored function from d365fo.tools
        Original Author: Mötz Jensen (@Splaxi)
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Invoke-ModelUtil {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Import', 'Export', 'Delete', 'Replace')]
        [string] $Command,

        [Parameter(Mandatory = $True, ParameterSetName = 'Import', Position = 1 )]
        [Parameter(Mandatory = $True, ParameterSetName = 'Export', Position = 1 )]
        [Alias('File')]
        [string] $Path,

        [Parameter(Mandatory = $True, ParameterSetName = 'Export', Position = 2 )]
        [Parameter(Mandatory = $True, ParameterSetName = 'Delete', Position = 1 )]
        [string] $Model,

        [string] $BinDir = "$Script:PackageDirectory\bin",

        [string] $MetaDataDir = "$Script:MetaDataDir",

        [string] $LogPath,

        [switch] $ShowOriginalProgress,

        [switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start
    
    if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) {
        Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1
    }

    $executable = Join-Path -Path $BinDir -ChildPath "ModelUtil.exe"
    if (-not (Test-PathExists -Path $executable -Type Leaf)) {
        Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1
    }

    $params = New-Object System.Collections.Generic.List[string]

    Write-PSFMessage -Level Verbose -Message "Building the parameter options."
    switch ($Command.ToLowerInvariant()) {
        'import' {
            if (-not (Test-PathExists -Path $Path -Type Leaf)) {
                Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1
            }

            $params.Add("-import")
            $params.Add("-metadatastorepath=`"$MetaDataDir`"")
            $params.Add("-file=`"$Path`"")
        }
        'export' {
            $params.Add("-export")
            $params.Add("-metadatastorepath=`"$MetaDataDir`"")
            $params.Add("-outputpath=`"$Path`"")
            $params.Add("-modelname=`"$Model`"")
        }
        'delete' {
            $params.Add("-delete")
            $params.Add("-metadatastorepath=`"$MetaDataDir`"")
            $params.Add("-modelname=`"$Model`"")
        }
        'replace' {
            if (-not (Test-PathExists -Path $Path -Type Leaf)) {
                Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1
            }

            $params.Add("-replace")
            $params.Add("-metadatastorepath=`"$MetaDataDir`"")
            $params.Add("-file=`"$Path`"")
            $params.Add("-force")
        }
        
    }

    Write-PSFMessage -Level Verbose -Message "Starting the $executable with the parameter options." -Target $($params.ToArray() -join " ")
    
    Invoke-Process -Executable $executable -Params $params.ToArray() -ShowOriginalProgress:$ShowOriginalProgress -OutputCommandOnly:$OutputCommandOnly -LogPath $LogPath

    if (Test-PSFFunctionInterrupt) {
        Stop-PSFFunction -Message "Stopping because of 'ModelUtil.exe' failed its execution." -StepsUpward 1
        return
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Invoke a process
         
    .DESCRIPTION
        Invoke a process and pass the needed parameters to it
         
    .PARAMETER Path
        Path to the program / executable that you want to start
         
    .PARAMETER Params
        Array of string parameters that you want to pass to the executable
         
    .PARAMETER LogPath
        The path where the log file(s) will be saved
         
    .PARAMETER ShowOriginalProgress
        Instruct the cmdlet to show the standard output in the console
         
        Default is $false which will silence the standard output
         
    .PARAMETER OutputCommandOnly
        Instruct the cmdlet to only output the command that you would have to execute by hand
         
        Will include full path to the executable and the needed parameters based on your selection
         
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions
        This is less user friendly, but allows catching exceptions in calling scripts
         
    .EXAMPLE
        PS C:\> Invoke-Process -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose"
         
        This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable.
        All parameters will be passed to it.
        The standard output will be redirected to a local variable.
        The error output will be redirected to a local variable.
        The standard output will be written to the verbose stream before exiting.
         
        If an error should occur, both the standard output and error output will be written to the console / host.
         
    .EXAMPLE
        PS C:\> Invoke-Process -ShowOriginalProgress -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose"
         
        This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable.
        All parameters will be passed to it.
        The standard output will be outputted directly to the console / host.
        The error output will be outputted directly to the console / host.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Invoke-Process {
    [CmdletBinding()]
    [OutputType()]
    param (
        [Parameter(Mandatory = $true)]
        [Alias('Executable')]
        [string] $Path,

        [Parameter(Mandatory = $true)]
        [string[]] $Params,
        [Parameter(Mandatory = $false)]
        [string] $LogPath,

        [switch] $ShowOriginalProgress,
        
        [switch] $OutputCommandOnly,

        [switch] $EnableException
    )

    Invoke-TimeSignal -Start

    if (-not (Test-PathExists -Path $Path -Type Leaf)) { return }
    
    if (Test-PSFFunctionInterrupt) { return }

    $tool = Split-Path -Path $Path -Leaf

    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = "$Path"
    $pinfo.WorkingDirectory = Split-Path -Path $Path -Parent

    if (-not $ShowOriginalProgress) {
        Write-PSFMessage -Level Verbose "Output and Error streams will be redirected (silence mode)"

        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
    }

    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = "$($Params -join " ")"
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo

    Write-PSFMessage -Level Verbose "Starting the $tool" -Target "$($params -join " ")"

    if ($OutputCommandOnly) {
        Write-PSFMessage -Level Host "$Path $($pinfo.Arguments)"
        return
    }
    
    $p.Start() | Out-Null
    
    if (-not $ShowOriginalProgress) {
        $outTask = $p.StandardOutput.ReadToEndAsync();
        $errTask = $p.StandardError.ReadToEndAsync();
    }

    Write-PSFMessage -Level Verbose "Waiting for the $tool to complete"
    $p.WaitForExit()

    if (-not $ShowOriginalProgress) {
        $stdout = $outTask.Result
        $stderr = $errTask.Result
    }

    if ($p.ExitCode -ne 0 -and (-not $ShowOriginalProgress)) {
        Write-PSFMessage -Level Host "Exit code from $tool indicated an error happened. Will output both standard stream and error stream."
        Write-PSFMessage -Level Host "Standard output was: \r\n $stdout"
        Write-PSFMessage -Level Host "Error output was: \r\n $stderr"

        $messageString = "Stopping because an Exit Code from $tool wasn't 0 (zero) like expected."
        Stop-PSFFunction -Message "Stopping because of Exit Code." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1
        return
    }
    else {
        Write-PSFMessage -Level Verbose "Standard output was: \r\n $stdout"
    }

    if ((-not $ShowOriginalProgress) -and (-not ([string]::IsNullOrEmpty($LogPath)))) {
        if (-not (Test-PathExists -Path $LogPath -Type Container -Create)) { return }

        $stdOutputPath = Join-Path -Path $LogPath -ChildPath "$tool`_StdOutput.log"
        $errOutputPath = Join-Path -Path $LogPath -ChildPath "$tool`_ErrOutput.log"

        $stdout | Out-File -FilePath $stdOutputPath -Encoding utf8 -Force
        $stderr | Out-File -FilePath $errOutputPath -Encoding utf8 -Force
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Handle time measurement
         
    .DESCRIPTION
        Handle time measurement from when a cmdlet / function starts and ends
         
        Will write the output to the verbose stream (Write-PSFMessage -Level Verbose)
         
    .PARAMETER Start
        Switch to instruct the cmdlet that a start time registration needs to take place
         
    .PARAMETER End
        Switch to instruct the cmdlet that a time registration has come to its end and it needs to do the calculation
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -Start
         
        This will start the time measurement for any given cmdlet / function
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -End
         
        This will end the time measurement for any given cmdlet / function.
        The output will go into the verbose stream.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Invoke-TimeSignal {
    [CmdletBinding(DefaultParameterSetName = 'Start')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Start', Position = 1 )]
        [switch] $Start,
        
        [Parameter(Mandatory = $True, ParameterSetName = 'End', Position = 2 )]
        [switch] $End
    )

    $Time = (Get-Date)

    $Command = (Get-PSCallStack)[1].Command

    if ($Start) {
        if ($Script:TimeSignals.ContainsKey($Command)) {
            Write-PSFMessage -Level Verbose -Message "The command '$Command' was already taking part in time measurement. The entry has been update with current date and time."
            $Script:TimeSignals[$Command] = $Time
        }
        else {
            $Script:TimeSignals.Add($Command, $Time)
        }
    }
    else {
        if ($Script:TimeSignals.ContainsKey($Command)) {
            $TimeSpan = New-TimeSpan -End $Time -Start (($Script:TimeSignals)[$Command])

            Write-PSFMessage -Level Verbose -Message "Total time spent inside the function was $TimeSpan" -Target $TimeSpan -FunctionName $Command -Tag "TimeSignal"
            $null = $Script:TimeSignals.Remove($Command)
        }
        else {
            Write-PSFMessage -Level Verbose -Message "The command '$Command' was never started to take part in time measurement."
        }
    }
}


<#
    .SYNOPSIS
        Short description
         
    .DESCRIPTION
        Long description
         
    .PARAMETER InputObject
        Parameter description
         
    .PARAMETER Property
        Parameter description
         
    .PARAMETER ExcludeProperty
        Parameter description
         
    .PARAMETER TypeName
        Parameter description
         
    .EXAMPLE
        PS C:\> Select-DefaultView -InputObject $result -Property CommandName, Synopsis
         
        This will help you do it right.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Select-DefaultView {
    <#
 
    This command enables us to send full on objects to the pipeline without the user seeing it
     
    a lot of this is from boe, thanks boe!
    https://learn-powershell.net/2013/08/03/quick-hits-set-the-default-property-display-in-powershell-on-custom-objects/
 
    TypeName creates a new type so that we can use ps1xml to modify the output
    #>

    
    [CmdletBinding()]
    param (
        [parameter(ValueFromPipeline)]
        [object]
        $InputObject,
        
        [string[]]
        $Property,
        
        [string[]]
        $ExcludeProperty,
        
        [string]
        $TypeName
    )
    process {
        
        if ($null -eq $InputObject) { return }
        
        if ($TypeName) {
            $InputObject.PSObject.TypeNames.Insert(0, "fscps.tools.$TypeName")
        }
        
        if ($ExcludeProperty) {
            if ($InputObject.GetType().Name.ToString() -eq 'DataRow') {
                $ExcludeProperty += 'Item', 'RowError', 'RowState', 'Table', 'ItemArray', 'HasErrors'
            }
            
            $props = ($InputObject | Get-Member | Where-Object MemberType -in 'Property', 'NoteProperty', 'AliasProperty' | Where-Object { $_.Name -notin $ExcludeProperty }).Name
            $defaultset = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$props)
        }
        else {
            # property needs to be string
            if ("$property" -like "* as *") {
                $newproperty = @()
                foreach ($p in $property) {
                    if ($p -like "* as *") {
                        $old, $new = $p -isplit " as "
                        # Do not be tempted to not pipe here
                        $inputobject | Add-Member -Force -MemberType AliasProperty -Name $new -Value $old -ErrorAction SilentlyContinue
                        $newproperty += $new
                    }
                    else {
                        $newproperty += $p
                    }
                }
                $property = $newproperty
            }
            $defaultset = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$Property)
        }
        
        $standardmembers = [System.Management.Automation.PSMemberInfo[]]@($defaultset)
        
        # Do not be tempted to not pipe here
        $inputobject | Add-Member -Force -MemberType MemberSet -Name PSStandardMembers -Value $standardmembers -ErrorAction SilentlyContinue
        
        $inputobject
    }
}


<#
    .SYNOPSIS
        Test accessible to the configuration storage
         
    .DESCRIPTION
        Test if the desired configuration storage is accessible with the current user context
         
    .PARAMETER ConfigStorageLocation
        Parameter used to instruct where to store the configuration objects
         
        The default value is "User" and this will store all configuration for the active user
         
        Valid options are:
        "User"
        "System"
         
        "System" will store the configuration so all users can access the configuration objects
         
    .EXAMPLE
        PS C:\> Test-ConfigStorageLocation -ConfigStorageLocation "System"
         
        This will test if the current executing user has enough privileges to save to the system wide configuration storage.
        The system wide configuration storage requires administrator rights.
         
    .NOTES
        Author: Mötz Jensen (@Splaxi)
         
#>

function Test-ConfigStorageLocation {
    [CmdletBinding()]
    [OutputType('System.String')]
    param (
        [ValidateSet('User', 'System')]
        [string] $ConfigStorageLocation = "User"
    )
    
    $configScope = "UserDefault"

    if ($ConfigStorageLocation -eq "System") {
        if ($Script:IsAdminRuntime) {
            $configScope = "SystemDefault"
        }
        else {
            Write-PSFMessage -Level Host -Message "Unable to locate save the <c='em'>configuration objects</c> in the <c='em'>system wide configuration store</c> on the machine. Please start an elevated session and run the cmdlet again."
            Stop-PSFFunction -Message "Elevated permissions needed. Please start an elevated session and run the cmdlet again." -StepsUpward 1
            return
        }
    }

    $configScope
}


<#
    .SYNOPSIS
        Test multiple paths
         
    .DESCRIPTION
        Easy way to test multiple paths for public functions and have the same error handling
         
    .PARAMETER Path
        Array of paths you want to test
         
        They have to be the same type, either file/leaf or folder/container
         
    .PARAMETER Type
        Type of path you want to test
         
        Either 'Leaf' or 'Container'
         
    .PARAMETER Create
        Instruct the cmdlet to create the directory if it doesn't exist
         
    .PARAMETER ShouldNotExist
        Instruct the cmdlet to return true if the file doesn't exists
         
    .EXAMPLE
        PS C:\> Test-PathExists "c:\temp","c:\temp\dir" -Type Container
         
        This will test if the mentioned paths (folders) exists and the current context has enough permission.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Test-PathExists {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $True)]
        [AllowEmptyString()]
        [string[]] $Path,

        [ValidateSet('Leaf', 'Container')]
        [Parameter(Mandatory = $True)]
        [string] $Type,

        [switch] $Create,

        [switch] $ShouldNotExist
    )
    
    $res = $false

    $arrList = New-Object -TypeName "System.Collections.ArrayList"
         
    foreach ($item in $Path) {

        if ([string]::IsNullOrEmpty($item)) {
            Stop-PSFFunction -Message "Stopping because path was either null or empty string." -StepsUpward 1
            return
        }

        Write-PSFMessage -Level Debug -Message "Testing the path: $item" -Target $item
        $temp = Test-Path -Path $item -Type $Type

        if ((-not $temp) -and ($Create) -and ($Type -eq "Container")) {
            Write-PSFMessage -Level Debug -Message "Creating the path: $item" -Target $item
            $null = New-Item -Path $item -ItemType Directory -Force -ErrorAction Stop
            $temp = $true
        }
        elseif ($ShouldNotExist) {
            Write-PSFMessage -Level Debug -Message "The should NOT exists: $item" -Target $item
        }
        elseif ((-not $temp) -and ($WarningPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue)) {
            Write-PSFMessage -Level Host -Message "The <c='em'>$item</c> path wasn't found. Please ensure the path <c='em'>exists</c> and you have enough <c='em'>permission</c> to access the path."
        }
        
        $null = $arrList.Add($temp)
    }

    if ($arrList.Contains($false) -and (-not $ShouldNotExist)) {
        # The $ErrorActionPreference variable determines the behavior we are after, but the "Stop-PSFFunction -WarningAction" is where we need to put in the value.
        Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 -WarningAction $ErrorActionPreference
        
    }
    elseif ($arrList.Contains($true) -and $ShouldNotExist) {
        # The $ErrorActionPreference variable determines the behavior we are after, but the "Stop-PSFFunction -WarningAction" is where we need to put in the value.
        Stop-PSFFunction -Message "Stopping because file exists." -StepsUpward 1 -WarningAction $ErrorActionPreference
    }
    else {
        $res = $true
    }

    $res
}


<#
    .SYNOPSIS
        Test if a given registry key exists or not
         
    .DESCRIPTION
        Test if a given registry key exists in the path specified
         
    .PARAMETER Path
        Path to the registry hive and sub directories you want to work against
         
    .PARAMETER Name
        Name of the registry key that you want to test for
         
    .EXAMPLE
        PS C:\> Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" -Name "InstallationInfoDirectory"
         
        This will query the LocalMachine hive and the sub directories "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" for a registry key with the name of "InstallationInfoDirectory".
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

Function Test-RegistryValue {
    [OutputType('System.Boolean')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    if (Test-Path -Path $Path -PathType Any) {
        $null -ne (Get-ItemProperty $Path).$Name
    }
    else {
        $false
    }
}


<#
    .SYNOPSIS
        Update the Azure Storage config variables
         
    .DESCRIPTION
        Update the active Azure Storage config variables that the module will use as default values
         
    .EXAMPLE
        PS C:\> Update-AzureStorageVariables
         
        This will update the Azure Storage variables.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Update-AzureStorageVariables {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType()]
    param ( )
    
    $hashParameters = Get-FSCPSActiveAzureStorageConfig

    foreach ($item in $hashParameters.Keys) {
            
        $name = "AzureStorage" + (Get-Culture).TextInfo.ToTitleCase($item)
        
        Write-PSFMessage -Level Verbose -Message "$name - $($hashParameters[$item])" -Target $hashParameters[$item]
        Set-Variable -Name $name -Value $hashParameters[$item] -Scope Script -Force
    }
}


<#
    .SYNOPSIS
        Update the broadcast message config variables
         
    .DESCRIPTION
        Update the active broadcast message config variables that the module will use as default values
         
    .EXAMPLE
        PS C:\> Update-BroadcastVariables
         
        This will update the broadcast variables.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Update-BroadcastVariables {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType()]
    param ( )

    $configName = (Get-PSFConfig -FullName "fscps.tools.active.broadcast.message.config.name").Value.ToString().ToLower()
    if (-not ($configName -eq "")) {
        $hashParameters = Get-FSCPSActiveBroadcastMessageConfig -OutputAsHashtable
        foreach ($item in $hashParameters.Keys) {
            if ($item -eq "name") { continue }
            
            $name = "Broadcast" + (Get-Culture).TextInfo.ToTitleCase($item)
        
            $valueMessage = $hashParameters[$item]

            if ($item -like "*client*" -and $valueMessage.Length -gt 20)
            {
                $valueMessage = $valueMessage.Substring(0,18) + "[...REDACTED...]"
            }

            Write-PSFMessage -Level Verbose -Message "$name - $valueMessage" -Target $valueMessage
            Set-Variable -Name $name -Value $hashParameters[$item] -Scope Script
        }
    }
}


<#
    .SYNOPSIS
        Update the LCS API config variables
         
    .DESCRIPTION
        Update the active LCS API config variables that the module will use as default values
         
    .EXAMPLE
        PS C:\> Update-LcsApiVariables
         
        This will update the LCS API variables.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Update-LcsApiVariables {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType()]
    param ( )
    
    $hashParameters = Get-D365LcsApiConfig -OutputAsHashtable

    foreach ($item in $hashParameters.Keys) {
            
        $name = "LcsApi" + (Get-Culture).TextInfo.ToTitleCase($item)
        
        $valueMessage = $hashParameters[$item]

        if ($item -like "*client*" -and $valueMessage.Length -gt 20)
        {
            $valueMessage = $valueMessage.Substring(0,18) + "[...REDACTED...]"
        }

        Write-PSFMessage -Level Verbose -Message "$name - $valueMessage" -Target $valueMessage
        Set-Variable -Name $name -Value $hashParameters[$item] -Scope Script
    }
}


<#
    .SYNOPSIS
        Update module variables
         
    .DESCRIPTION
        Loads configuration variables again, to make sure things are updated based on changed configuration
         
    .EXAMPLE
        PS C:\> Update-ModuleVariables
         
        This will update internal variables that the module is dependent on.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Update-ModuleVariables {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType()]
    param ( )

    Update-PsfConfigVariables

    $Script:AADOAuthEndpoint = Get-PSFConfigValue -FullName "fscps.tools.azure.common.oauth.token"
}


<#
    .SYNOPSIS
        Update the module variables based on the PSF Configuration store
         
    .DESCRIPTION
        Will read the current PSF Configuration store and create local module variables
         
    .EXAMPLE
        PS C:\> Update-PsfConfigVariables
         
        This will read all relevant PSF Configuration values and create matching module variables.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Update-PsfConfigVariables {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]

    [CmdletBinding()]
    [OutputType()]
    param ()

    foreach ($config in Get-PSFConfig -FullName "fscps.tools.path.*") {
        $item = $config.FullName.Replace("fscps.tools.path.", "")
        $name = (Get-Culture).TextInfo.ToTitleCase($item) + "Path"
        
        Set-Variable -Name $name -Value $config.Value -Scope Script
    }
}


<#
    .SYNOPSIS
        Update the topology file
         
    .DESCRIPTION
        Update the topology file based on the already installed list of services on the machine
         
    .PARAMETER Path
        Path to the folder where the topology XML file that you want to work against is placed
         
        Should only contain a path to a folder, not a file
         
    .EXAMPLE
        PS C:\> Update-TopologyFile -Path "c:\temp\fscps.tools\DefaultTopologyData.xml"
         
        This will update the "c:\temp\fscps.tools\DefaultTopologyData.xml" file with all the installed services on the machine.
         
    .NOTES
        # Credit http://dev.goshoom.net/en/2016/11/installing-deployable-packages-with-powershell/
         
        Author: Tommy Skaue (@Skaue)
        Author: Mötz Jensen (@Splaxi)
         
#>

function Update-TopologyFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )
    
    $topologyFile = Join-Path $Path 'DefaultTopologyData.xml'
                
    Write-PSFMessage -Level Verbose "Creating topology file: $topologyFile"
                
    [xml]$xml = Get-Content $topologyFile
    $machine = $xml.TopologyData.MachineList.Machine
    $machine.Name = $env:computername
                
    $serviceModelList = $machine.ServiceModelList
    $null = $serviceModelList.RemoveAll()
 
    [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList"

    $null = $Files2Process.Add((Join-Path $Path 'Microsoft.Dynamics.AX.AXInstallationInfo.dll'))
    Import-AssemblyFileIntoMemory -Path $($Files2Process.ToArray())
 
    $models = [Microsoft.Dynamics.AX.AXInstallationInfo.AXInstallationInfo]::GetInstalledServiceModel()

    foreach ($name in $models.Name) {
        $element = $xml.CreateElement('string')
        $element.InnerText = $name
        $serviceModelList.AppendChild($element)
    }
    
    $xml.Save($topologyFile)
    
    $true
}


<#
    .SYNOPSIS
        Save an Azure Storage Account config
         
    .DESCRIPTION
        Adds an Azure Storage Account config to the configuration store
         
    .PARAMETER Name
        The logical name of the Azure Storage Account you are about to registered in the configuration store
         
    .PARAMETER AccountId
        The account id for the Azure Storage Account you want to register in the configuration store
         
    .PARAMETER AccessToken
        The access token for the Azure Storage Account you want to register in the configuration store
         
    .PARAMETER SAS
        The SAS key that you have created for the storage account or blob container
         
    .PARAMETER Container
        The name of the blob container inside the Azure Storage Account you want to register in the configuration store
         
    .PARAMETER Temporary
        Instruct the cmdlet to only temporarily add the azure storage account configuration in the configuration store
         
    .PARAMETER Force
        Switch to instruct the cmdlet to overwrite already registered Azure Storage Account entry
         
    .EXAMPLE
        PS C:\> Add-FSCPSAzureStorageConfig -Name "UAT-Exports" -AccountId "1234" -AccessToken "dafdfasdfasdf" -Container "testblob"
         
        This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", AccessToken "dafdfasdfasdf" and blob container "testblob".
         
    .EXAMPLE
        PS C:\> Add-FSCPSAzureStorageConfig -Name UAT-Exports -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -AccountId "1234" -Container "testblob"
         
        This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", SAS "sv=2018-03-28&si=unlisted&sr=c&sig=AUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" and blob container "testblob".
        The SAS key enables you to provide explicit access to a given blob container inside an Azure Storage Account.
        The SAS key can easily be revoked and that way you have control over the access to the container and its content.
         
    .EXAMPLE
        PS C:\> Add-FSCPSAzureStorageConfig -Name UAT-Exports -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -AccountId "1234" -Container "testblob" -Temporary
         
        This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", SAS "sv=2018-03-28&si=unlisted&sr=c&sig=AUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" and blob container "testblob".
        The SAS key enables you to provide explicit access to a given blob container inside an Azure Storage Account.
        The SAS key can easily be revoked and that way you have control over the access to the container and its content.
         
        The configuration will only last for the rest of this PowerShell console session.
         
    .NOTES
        Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container
         
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Add-FSCPSAzureStorageConfig {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $Name,

        [Parameter(Mandatory = $true)]
        [string] $AccountId,

        [Parameter(Mandatory = $true, ParameterSetName = "AccessToken")]
        [string] $AccessToken,

        [Parameter(Mandatory = $true, ParameterSetName = "SAS")]
        [string] $SAS,

        [Parameter(Mandatory = $true)]
        [Alias('Blob')]
        [Alias('Blobname')]
        [string] $Container,

        [switch] $Temporary,

        [switch] $Force
    )
    
    $Details = @{AccountId = $AccountId.ToLower();
        Container          = $Container.ToLower();
    }

    if ($PSCmdlet.ParameterSetName -eq "AccessToken") { $Details.AccessToken = $AccessToken }
    if ($PSCmdlet.ParameterSetName -eq "SAS") {
        if ($SAS.StartsWith("?")) {
            $SAS = $SAS.Substring(1)
        }

        $Details.SAS = $SAS
    }

    $Accounts = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts")

    if(-not $Accounts)
    {
        $Accounts = @{}
    }

    if ($Accounts.ContainsKey($Name)) {
        if ($Force) {
            $Accounts[$Name] = $Details

            Set-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Value $Accounts
        }
        else {
            Write-PSFMessage -Level Host -Message "An Azure Storage Account with that name <c='em'>already exists</c>. If you want to <c='em'>overwrite</c> the already registered details please supply the <c='em'>-Force</c> parameter."
            Stop-PSFFunction -Message "Stopping because an Azure Storage Account already exists with that name."
            return
        }
    }
    else {
        $null = $Accounts.Add($Name, $Details)

        Set-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Value $Accounts
    }

    if (-not $Temporary) { Register-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Scope UserDefault }
}


<#
    .SYNOPSIS
        Disables throwing of exceptions
         
    .DESCRIPTION
        Restore the default exception behavior of the module to not support throwing exceptions
         
        Useful when the default behavior was changed with Enable-FSCPSException and the default behavior should be restored
         
    .EXAMPLE
        PS C:\>Disable-FSCPSException
         
        This will restore the default behavior of the module to not support throwing exceptions.
         
    .NOTES
        Tags: Exception, Exceptions, Warning, Warnings
        This is refactored function from d365fo.tools
         
        Original Author: Florian Hopfner (@FH-Inway)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
    .LINK
        Enable-FSCPSException
#>


function Disable-FSCPSException {
    [CmdletBinding()]
    param ()

    Write-PSFMessage -Level Verbose -Message "Disabling exception across the entire module." -Target $configurationValue

    Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $false -ModuleName "fscps.tools"
    Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $false -ModuleName "PSOAuthHelper"
    $PSDefaultParameterValues['*:EnableException'] = $false
}


<#
    .SYNOPSIS
        Enable exceptions to be thrown
         
    .DESCRIPTION
        Change the default exception behavior of the module to support throwing exceptions
         
        Useful when the module is used in an automated fashion, like inside Azure DevOps pipelines and large PowerShell scripts
         
    .EXAMPLE
        PS C:\>Enable-FSCPSException
         
        This will for the rest of the current PowerShell session make sure that exceptions will be thrown.
         
    .NOTES
        Tags: Exception, Exceptions, Warning, Warnings
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
    .LINK
        Disable-FSCPSException
#>


function Enable-FSCPSException {
    [CmdletBinding()]
    param ()

    Write-PSFMessage -Level Verbose -Message "Enabling exception across the entire module." -Target $configurationValue

    Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $true -ModuleName "fscps.tools"
    Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $true -ModuleName "PSOAuthHelper"
    $PSDefaultParameterValues['*:EnableException'] = $true
}


<#
    .SYNOPSIS
        Finds fscps.tools commands searching through the inline help text
         
    .DESCRIPTION
        Finds fscps.tools commands searching through the inline help text, building a consolidated json index and querying it because Get-Help is too slow
         
    .PARAMETER Tag
        Finds all commands tagged with this auto-populated tag
         
    .PARAMETER Author
        Finds all commands tagged with this author
         
    .PARAMETER MinimumVersion
        Finds all commands tagged with this auto-populated minimum version
         
    .PARAMETER MaximumVersion
        Finds all commands tagged with this auto-populated maximum version
         
    .PARAMETER Rebuild
        Rebuilds the index
         
    .PARAMETER Pattern
        Searches help for all commands in fscps.tools for the specified pattern and displays all results
         
    .PARAMETER Confirm
        Confirms overwrite of index
         
    .PARAMETER WhatIf
        Displays what would happen if the command is run
         
    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.
         
    .EXAMPLE
        PS C:\> Find-FSCPSCommand "snapshot"
         
        For lazy typers: finds all commands searching the entire help for "snapshot"
         
    .EXAMPLE
        PS C:\> Find-FSCPSCommand -Pattern "snapshot"
         
        For rigorous typers: finds all commands searching the entire help for "snapshot"
         
    .EXAMPLE
        PS C:\> Find-FSCPSCommand -Tag copy
         
        Finds all commands tagged with "copy"
         
    .EXAMPLE
        PS C:\> Find-FSCPSCommand -Tag copy,user
         
        Finds all commands tagged with BOTH "copy" and "user"
         
    .EXAMPLE
        PS C:\> Find-FSCPSCommand -Author Mötz
         
        Finds every command whose author contains "Mötz"
         
    .EXAMPLE
        PS C:\> Find-FSCPSCommand -Author Mötz -Tag copy
         
        Finds every command whose author contains "Mötz" and it tagged as "copy"
         
    .EXAMPLE
        PS C:\> Find-FSCPSCommand -Rebuild
         
        Finds all commands and rebuilding the index (good for developers)
         
         
    .NOTES
        Tags: Find, Help, Command
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
        License: MIT https://opensource.org/licenses/MIT
         
        This cmdlet / function is copy & paste implementation based on the Find-DbaCommand from the dbatools.io project
         
        Original author: Simone Bizzotto (@niphold)
         
#>

function Find-FSCPSCommand {

        [CmdletBinding(SupportsShouldProcess = $true)]
        param (
            [String]$Pattern,
            [String[]]$Tag,
            [String]$Author,
            [String]$MinimumVersion,
            [String]$MaximumVersion,
            [switch]$Rebuild,
            [Alias('Silent')]
            [switch]$EnableException
        )
        begin {
            function Get-FSCPSTrimmedString($Text) {
                return $Text.Trim() -replace '(\r\n){2,}', "`n"
            }
    
            $tagsRex = ([regex]'(?m)^[\s]{0,15}Tags:(.*)$')
            $authorRex = ([regex]'(?m)^[\s]{0,15}Author:(.*)$')
            $minverRex = ([regex]'(?m)^[\s]{0,15}MinimumVersion:(.*)$')
            $maxverRex = ([regex]'(?m)^[\s]{0,15}MaximumVersion:(.*)$')
    
            function Get-FSCPSHelp([String]$commandName) {
                $thishelp = Get-Help $commandName -Full
                $thebase = @{ }
                $thebase.CommandName = $commandName
                $thebase.Name = $thishelp.Name
    
                $alias = Get-Alias -Definition $commandName -ErrorAction SilentlyContinue
                $thebase.Alias = $alias.Name -Join ','
    
                ## fetch the description
                $thebase.Description = $thishelp.Description.Text
    
                ## fetch examples
                $thebase.Examples = Get-FSCPSTrimmedString -Text ($thishelp.Examples | Out-String -Width 200)
    
                ## fetch help link
                $thebase.Links = ($thishelp.relatedLinks).NavigationLink.Uri
    
                ## fetch the synopsis
                $thebase.Synopsis = $thishelp.Synopsis
    
                ## fetch the syntax
                $thebase.Syntax = Get-FSCPSTrimmedString -Text ($thishelp.Syntax | Out-String -Width 600)
    
                ## store notes
                $as = $thishelp.AlertSet | Out-String -Width 600
    
                ## fetch the tags
                $tags = $tagsrex.Match($as).Groups[1].Value
                if ($tags) {
                    $thebase.Tags = $tags.Split(',').Trim()
                }
                ## fetch the author
                $author = $authorRex.Match($as).Groups[1].Value
                if ($author) {
                    $thebase.Author = $author.Trim()
                }
    
                ## fetch MinimumVersion
                $MinimumVersion = $minverRex.Match($as).Groups[1].Value
                if ($MinimumVersion) {
                    $thebase.MinimumVersion = $MinimumVersion.Trim()
                }
    
                ## fetch MaximumVersion
                $MaximumVersion = $maxverRex.Match($as).Groups[1].Value
                if ($MaximumVersion) {
                    $thebase.MaximumVersion = $MaximumVersion.Trim()
                }
    
                ## fetch Parameters
                $parameters = $thishelp.parameters.parameter
                $command = Get-Command $commandName
                $params = @()
                foreach($p in $parameters) {
                    $paramAlias = $command.parameters[$p.Name].Aliases
                    $paramDescr = Get-FSCPSTrimmedString -Text ($p.Description | Out-String -Width 200)
                    $params += , @($p.Name, $paramDescr, ($paramAlias -Join ','), ($p.Required -eq $true), $p.PipelineInput, $p.DefaultValue)
                }
    
                $thebase.Params = $params
    
                [pscustomobject]$thebase
            }
    
            function Get-FSCPSIndex() {
                if ($Pscmdlet.ShouldProcess($dest, "Recreating index")) {
                    $dbamodule = Get-Module -Name fscps.tools
                    $allCommands = $dbamodule.ExportedCommands.Values | Where-Object CommandType -EQ 'Function'
    
                    $helpcoll = New-Object System.Collections.Generic.List[System.Object]
                    foreach ($command in $allCommands) {
                        $x = Get-FSCPSHelp "$command"
                        $helpcoll.Add($x)
                    }
                    # $dest = Get-DbatoolsConfigValue -Name 'Path.TagCache' -Fallback "$(Resolve-Path $PSScriptRoot\..)\dbatools-index.json"
                    $dest = "$moduleDirectory\bin\fscps.tools-index.json"
                    $helpcoll | ConvertTo-Json -Depth 4 | Out-File $dest -Encoding UTF8
                }
            }
    
            $moduleDirectory = (Get-Module -Name fscps.tools).ModuleBase
        }
        process {
            $Pattern = $Pattern.TrimEnd("s")
            $idxFile = "$moduleDirectory\bin\fscps.tools-index.json"
            if (!(Test-Path $idxFile) -or $Rebuild) {
                Write-PSFMessage -Level Verbose -Message "Rebuilding index into $idxFile"
                $swRebuild = [system.diagnostics.stopwatch]::StartNew()
                Get-FSCPSIndex
                Write-PSFMessage -Level Verbose -Message "Rebuild done in $($swRebuild.ElapsedMilliseconds)ms"
            }
            $consolidated = Get-Content -Raw $idxFile | ConvertFrom-Json
            $result = $consolidated
            if ($Pattern.Length -gt 0) {
                $result = $result | Where-Object { $_.PsObject.Properties.Value -like "*$Pattern*" }
            }
    
            if ($Tag.Length -gt 0) {
                foreach ($t in $Tag) {
                    $result = $result | Where-Object Tags -Contains $t
                }
            }
    
            if ($Author.Length -gt 0) {
                $result = $result | Where-Object Author -Like "*$Author*"
            }
    
            if ($MinimumVersion.Length -gt 0) {
                $result = $result | Where-Object MinimumVersion -GE $MinimumVersion
            }
    
            if ($MaximumVersion.Length -gt 0) {
                $result = $result | Where-Object MaximumVersion -LE $MaximumVersion
            }
    
            Select-DefaultView -InputObject $result -Property CommandName, Synopsis
        }
    }


<#
    .SYNOPSIS
        Get active Azure Storage Account configuration
         
    .DESCRIPTION
        Get active Azure Storage Account configuration object from the configuration store
         
    .PARAMETER OutputAsPsCustomObject
        Instruct the cmdlet to return a PsCustomObject object
         
    .EXAMPLE
        PS C:\> Get-FSCPSActiveAzureStorageConfig
         
        This will get the active Azure Storage configuration.
         
    .EXAMPLE
        PS C:\> Get-FSCPSActiveAzureStorageConfig -OutputAsPsCustomObject:$true
         
        This will get the active Azure Storage configuration.
        The object will be output as a PsCustomObject, for you to utilize across your scripts.
         
    .NOTES
        Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container
         
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Get-FSCPSActiveAzureStorageConfig {
    [CmdletBinding()]
    param (
        [switch] $OutputAsPsCustomObject
    )

    $res = Get-PSFConfigValue -FullName "fscps.tools.active.azure.storage.account"

    if ($OutputAsPsCustomObject) {
        [PSCustomObject]$res
    }
    else {
        $res
    }
}


<#
    .SYNOPSIS
        Get Azure Storage Account configs
         
    .DESCRIPTION
        Get all Azure Storage Account configuration objects from the configuration store
         
    .PARAMETER Name
        The name of the Azure Storage Account you are looking for
         
        Default value is "*" to display all Azure Storage Account configs
         
    .PARAMETER OutputAsHashtable
        Instruct the cmdlet to return a hastable object
         
    .EXAMPLE
        PS C:\> Get-FSCPSAzureStorageConfig
         
        This will show all Azure Storage Account configs
         
    .EXAMPLE
        PS C:\> Get-FSCPSAzureStorageConfig -OutputAsHashtable
         
        This will show all Azure Storage Account configs.
        Every object will be output as a hashtable, for you to utilize as parameters for other cmdlets.
         
    .NOTES
        Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container
         
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>

function Get-FSCPSAzureStorageConfig {
    [CmdletBinding()]
    param (
        [string] $Name = "*",

        [switch] $OutputAsHashtable
    )
    
    $StorageAccounts = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts")
     
    if(!$StorageAccounts)
    {
        Init-AzureStorageDefault
        $StorageAccounts = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts")
    }

    foreach ($item in $StorageAccounts.Keys) {
        if ($item -NotLike $Name) { continue }
        $res = [ordered]@{Name = $item }
        $res += $StorageAccounts[$item]

        if ($OutputAsHashtable) {
            $res
        }
        else {
            [PSCustomObject]$res
        }
    }
}


<#
    .SYNOPSIS
        Get a file from Azure
         
    .DESCRIPTION
        Get all files from an Azure Storage Account
         
    .PARAMETER AccountId
        Storage Account Name / Storage Account Id where you want to look for files
         
    .PARAMETER AccessToken
        The token that has the needed permissions for the search action
         
    .PARAMETER SAS
        The SAS key that you have created for the storage account or blob container
         
    .PARAMETER DestinationPath
        The destination folder of the Azure file to download. If enpty just show the info
         
    .PARAMETER Container
        Name of the blob container inside the storage account you want to look for files
         
    .PARAMETER Name
        Name of the file you are looking for
         
        Accepts wildcards for searching. E.g. -Name "Application*Adaptor"
         
        Default value is "*" which will search for all packages
         
    .PARAMETER Latest
        Instruct the cmdlet to only fetch the latest file from the Azure Storage Account
         
        Latest nugets parameter
         
    .EXAMPLE
        PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles"
         
        This will get all files in the blob container "backupfiles".
        It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access.
         
    .EXAMPLE
        PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Latest
         
        This will get the latest (newest) file from the blob container "backupfiles".
        It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access to the container.
         
    .EXAMPLE
        PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Name "*UAT*"
         
        This will get all files in the blob container "backupfiles" that fits the "*UAT*" search value.
        It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access to the container.
         
    .EXAMPLE
        PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Latest
         
        This will get the latest (newest) file from the blob container "backupfiles".
        It will use the SAS key "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" to gain access to the container.
         
    .NOTES
        Tags: Azure, Azure Storage, Token, Blob, File, Container
         
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Get-FSCPSAzureStorageFile {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [string] $AccountId = $Script:AzureStorageAccountId,

        [string] $AccessToken = $Script:AzureStorageAccessToken,

        [string] $SAS = $Script:AzureStorageSAS,

        [Alias('Blob')]
        [Alias('Blobname')]
        [string] $Container = $Script:AzureStorageContainer,

        [Parameter(ParameterSetName = 'Default')]
        [Alias('FileName')]
        [string] $Name = "*",

        [Parameter(ParameterSetName = 'Default')]
        [string] $DestinationPath = "",

        [Parameter(Mandatory = $true, ParameterSetName = 'Latest')]
        [Alias('GetLatest')]
        [switch] $Latest
    )

    if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or
        ([string]::IsNullOrEmpty($Container)) -or
        (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) {
        Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved."
        Stop-PSFFunction -Message "Stopping because of missing parameters"
        return
    }

    Invoke-TimeSignal -Start

    if ([string]::IsNullOrEmpty($SAS)) {
        Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken"

        $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken
    }
    else {
        Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS"

        $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS)
        $storageContext = New-AzStorageContext -ConnectionString $conString
    }

    try {
        $files = Get-AzStorageBlob -Container $($Container.ToLower()) -Context $storageContext | Sort-Object -Descending { $_.Properties.LastModified }

        if ($Latest) {
            $files | Select-Object -First 1 | Select-PSFObject -TypeName FSCPS.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, @{Name = "LastModified"; Expression = { [Datetime]::Parse($_.LastModified) } }
        }
        else {
    
            foreach ($obj in $files) {
                if ($obj.Name -NotLike $Name) { continue }

                if($DestinationPath)
                {
                    $null = Test-PathExists -Path $DestinationPath -Type Container -Create
                    $destinationBlobPath = (Join-Path $DestinationPath ($obj.Name))
                    Get-AzStorageBlobContent -Context $storageContext -Container $($Container.ToLower()) -Blob $obj.Name -Destination ($destinationBlobPath) -ConcurrentTaskCount 10 -Force
                    $obj | Select-PSFObject -TypeName FSCPS.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, @{Name = "Path"; Expression = { [string]$destinationBlobPath } }, @{Name = "LastModified"; Expression = { [Datetime]::Parse($_.LastModified) } }
                }
                else
                {
                    $obj | Select-PSFObject -TypeName FSCPS.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, @{Name = "LastModified"; Expression = { [Datetime]::Parse($_.LastModified) } }
                }
                
                
            }
        }
    }
    catch {
        Write-PSFMessage -Level Warning -Message "Something broke" -ErrorRecord $_
    }
}


<#
    .SYNOPSIS
        Get the D365FSC NuGet package
         
    .DESCRIPTION
        Get the D365FSC NuGet package from storage account
         
        Full list of NuGet: https://lcs.dynamics.com/V2/SharedAssetLibrary and select NuGet packages
         
    .PARAMETER Version
        The version of the NuGet package to download
         
    .PARAMETER Type
        The type of the NuGet package to download
         
    .PARAMETER Path
        The destination folder of the NuGet package to download
         
    .PARAMETER Force
        Instruct the cmdlet to override the package if exists
         
    .EXAMPLE
        PS C:\> Get-FSCPSNuget -Version "10.0.1777.99" -Type PlatformCompilerPackage
         
        This will download the NuGet package with version "10.0.1777.99" and type "PlatformCompilerPackage" to the current folder
         
    .EXAMPLE
        PS C:\> Get-FSCPSNuget -Version "10.0.1777.99" -Type PlatformCompilerPackage -Path "c:\temp"
         
        This will download the NuGet package with version "10.0.1777.99" and type "PlatformCompilerPackage" to the c:\temp folder
         
    .EXAMPLE
        PS C:\> Get-FSCPSNuget -Version "10.0.1777.99" -Type PlatformCompilerPackage -Path "c:\temp" -Force
         
        This will download the NuGet package with version "10.0.1777.99" and type "PlatformCompilerPackage" to the c:\temp folder and override if the package with the same name exists.
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Get-FSCPSNuget {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignment", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $Version,
        [Parameter(Mandatory = $true)]
        [NuGetType] $Type,
        [string] $Path,
        [switch] $Force
    )

    BEGIN {
        Invoke-TimeSignal -Start
        $packageName = ""
        switch ($Type) {
            ([NugetType]::ApplicationSuiteDevALM)
            { 
                $packageName = "Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp.$Version.nupkg"
                break;
            }
            ([NugetType]::ApplicationDevALM)
            { 
                $packageName = "Microsoft.Dynamics.AX.Application.DevALM.BuildXpp.$Version.nupkg"
                break;
            }
           ([NugetType]::PlatformDevALM)
            { 
                $packageName = "Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp.$Version.nupkg"
                break;
            }
            ([NugetType]::PlatformCompilerPackage)
            { 
                $packageName = "Microsoft.Dynamics.AX.Platform.CompilerPackage.$Version.nupkg"
                break;
            }
            Default {}
        }

        $storageConfigs = Get-FSCPSAzureStorageConfig
        $activeStorageConfigName = "NugetStorage"
        if($storageConfigs.Length -gt 0)
        {
            $activeStorageConfig = Get-FSCPSActiveAzureStorageConfig
            $storageConfigs | ForEach-Object {
                if($_.AccountId -eq $activeStorageConfig.AccountId -and $_.Container -eq $activeStorageConfig.Container -and $_.SAS -eq $activeStorageConfig.SAS)
                {
                    $activeStorageConfigName = $_.Name
                }
            }
        }
        Write-PSFMessage -Level Verbose -Message "ActiveStorageConfigName: $activeStorageConfigName"
        if($Force)
        {
            $null = Test-PathExists $Path -Create -Type Container 
        }
        else{
            $null = Test-PathExists $Path -Type Container
        }
    }
    
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        try {
            Set-FSCPSActiveAzureStorageConfig "NuGetStorage" -ErrorAction SilentlyContinue
            $destinationNugetFilePath = Join-Path $Path $packageName 
            
            $download = (-not(Test-Path $destinationNugetFilePath))

            if(!$download)
            {
                Write-PSFMessage -Level Host -Message $packageName
                $blobFile = Get-FSCPSAzureStorageFile -Name $packageName
                $blobSize = $blobFile.Length
                $localSize = (Get-Item $destinationNugetFilePath).length
                Write-PSFMessage -Level Verbose -Message "BlobSize is: $blobSize"
                Write-PSFMessage -Level Verbose -Message "LocalSize is: $blobSize"
                $download = $blobSize -ne $localSize
            }

            if($Force)
            {
                $download = $true
            }

            if($download)
            {
                Invoke-FSCPSAzureStorageDownload -FileName $packageName -Path $Path -Force:$Force
            }
        }
        catch {            
            Write-PSFMessage -Level Host -Message "Something went wrong while downloading NuGet package" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors"
            return
        }
        finally{
            if((Get-FSCPSAzureStorageConfig $activeStorageConfigName -ErrorAction SilentlyContinue).Length -gt 0){
                Set-FSCPSActiveAzureStorageConfig $activeStorageConfigName -ErrorAction SilentlyContinue
            }
            else
            {
                Set-FSCPSActiveAzureStorageConfig "NuGetStorage" -ErrorAction SilentlyContinue
            }       
        }
    }
    END {
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        Get the FSCPS configuration details
         
    .DESCRIPTION
        Get the FSCPS configuration details from the configuration store
         
        All settings retrieved from this cmdlets is to be considered the default parameter values across the different cmdlets
         
    .PARAMETER SettingsJsonString
        String contains settings JSON
         
    .PARAMETER SettingsJsonPath
        String contains path to the settings.json
         
    .PARAMETER OutputAsHashtable
        Instruct the cmdlet to return a hashtable object
         
    .EXAMPLE
        PS C:\> Get-FSCPSSettings
         
        This will output the current FSCPS configuration.
        The object returned will be a PSCustomObject.
         
    .EXAMPLE
        PS C:\> Get-FSCPSSettings -OutputAsHashtable
         
        This will output the current FSCPS configuration.
        The object returned will be a Hashtable.
         
    .LINK
        Set-FSCPSSettings
         
    .NOTES
        Tags: Environment, Url, Config, Configuration, LCS, Upload, ClientId
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Get-FSCPSSettings {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    param (
        [string] $SettingsJsonString,
        [string] $SettingsJsonPath,
        [switch] $OutputAsHashtable
    )
    begin{
        Invoke-TimeSignal -Start   
        $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve
        . ($helperPath)    
        $res = [Ordered]@{}

        if((-not ($SettingsJsonString -eq "")) -and (-not ($SettingsJsonPath -eq "")))
        {
            throw "Both settings parameters should not be provided. Please provide only one of them."
        }

        if(-not ($SettingsJsonString -eq ""))
        {
            $tmpSettingsFilePath = "C:\temp\settings.json"
            $null = Test-PathExists -Path "C:\temp\" -Type Container -Create
            $null = Set-Content $tmpSettingsFilePath $SettingsJsonString -Force -PassThru
            $null = Set-FSCPSSettings -SettingsFilePath $tmpSettingsFilePath
        }

        if(-not ($SettingsJsonPath -eq ""))
        {
            $null = Set-FSCPSSettings -SettingsFilePath $SettingsJsonPath
        }        
    }
    process{         

        foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.all.*") {
            $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.all.", "")
            $res.$propertyName = $config.Value
        }
        if($Script:IsOnGitHub)# If GitHub context
        {
            foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.github.*") {
                $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.github.", "")
                $res.$propertyName = $config.Value
            }
        }
        if($Script:IsOnAzureDevOps)# If ADO context
        {
            foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.ado.*") {
                $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.ado.", "")
                $res.$propertyName = $config.Value
            }
        }
        if($Script:IsOnLocalhost)# If localhost context
        {
            foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.localhost.*") {
                $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.localhost.", "")
                $res.$propertyName = $config.Value
            }
        }
        if($OutputAsHashtable) {
            $res
        } else {
            [PSCustomObject]$res
        }   
       
    }
    end{
        Invoke-TimeSignal -End
    }

}


<#
    .SYNOPSIS
        Get the list of D365FSC components versions
         
    .DESCRIPTION
        Get the list of D365FSC components versions (NuGets, Packages, Frameworks etc.)
         
         
    .PARAMETER Version
        The version of the D365FSC
         
    .EXAMPLE
        PS C:\> Get-FSCPSVersionInfo -Version "10.0.39"
         
        This will show the list of file versions for the FSCPS module of the 10.0.39 D365FSC.
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


Function Get-FSCPSVersionInfo {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
    [CmdletBinding()]
    param (
        [string] $Version
    )

    BEGIN {
        Invoke-TimeSignal -Start
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        $VersionStrategy = Get-PSFConfigValue -FullName "fscps.tools.settings.all.versionStrategy"
        $versionsDefaultFile = Join-Path "$Script:DefaultTempPath" "versions.default.json"

        try {
            Invoke-FSCPSWebRequest -method GET -Uri "https://raw.githubusercontent.com/fscpscollaborative/fscps/main/Actions/Helpers/versions.default.json" -outFile $versionsDefaultFile
        }
        catch {
            Start-BitsTransfer -Source "https://raw.githubusercontent.com/fscpscollaborative/fscps/main/Actions/Helpers/versions.default.json" -Destination $versionsDefaultFile
        }       
        
        $versionsData = (Get-Content $versionsDefaultFile) | ConvertFrom-Json 

        # TODO CREATE GETPROJECTROOTFOLDER function
        <#
        $versionsFile = Join-Path $ENV:GITHUB_WORKSPACE '.FSC-PS\versions.json'
         
        if(Test-Path $versionsFile)
        {
            $versions = (Get-Content $versionsFile) | ConvertFrom-Json
            ForEach($version in $versions)
            {
                ForEach($versionDefault in $versionsData)
                {
                    if($version.version -eq $versionDefault.version)
                    {
             
                        if($version.data.PSobject.Properties.name -match "AppVersion")
                        {
                            if($version.data.AppVersion -ne "")
                            {
                                $versionDefault.data.AppVersion = $version.data.AppVersion
                            }
                        }
                        if($version.data.PSobject.Properties.name -match "PlatformVersion")
                        {
                            if($version.data.PlatformVersion -ne "")
                            {
                                $versionDefault.data.PlatformVersion = $version.data.PlatformVersion
                            }
                        }
                    }
                }
            }
        }
        #>

    }
    
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }      

        try {
            if($Version)
            {
                foreach($d in $versionsData)
                {
                    if($d.version -eq $Version)
                    {
                        $hash = @{
                            version = $Version
                            data = @{
                                AppVersion                      = $( if($VersionStrategy -eq 'GA') { $d.data.AppVersionGA } else { $d.data.AppVersionLatest } )
                                PlatformVersion                 = $( if($VersionStrategy -eq 'GA') { $d.data.PlatformVersionGA  } else { $d.data.PlatformVersionLatest } )
                                FSCServiseUpdatePackageId       = $d.data.fscServiseUpdatePackageId
                                FSCPreviewVersionPackageId      = $d.data.fscPreviewVersionPackageId
                                FSCLatestQualityUpdatePackageId = $d.data.fscLatestQualityUpdatePackageId
                                FSCFinalQualityUpdatePackageId  = $d.data.fscFinalQualityUpdatePackageId
                                ECommerceMicrosoftRepoBranch    = $d.data.ecommerceMicrosoftRepoBranch
                            }
                        }                             
                        New-Object PSObject -Property $hash | Select-PSFObject -TypeName "FSCPS.TOOLS.Versions" "*"
                    }
                }
            }
            else
            {
                foreach($d in $versionsData)
                {
                        $hash = @{
                            version = $d.version
                            data = @{
                                AppVersion                      = $( if($VersionStrategy -eq 'GA') { $d.data.AppVersionGA } else { $d.data.AppVersionLatest } )
                                PlatformVersion                 = $( if($VersionStrategy -eq 'GA') { $d.data.PlatformVersionGA  } else { $d.data.PlatformVersionLatest } )
                                FSCServiseUpdatePackageId       = $d.data.fscServiseUpdatePackageId
                                FSCPreviewVersionPackageId      = $d.data.fscPreviewVersionPackageId
                                FSCLatestQualityUpdatePackageId = $d.data.fscLatestQualityUpdatePackageId
                                FSCFinalQualityUpdatePackageId  = $d.data.fscFinalQualityUpdatePackageId
                                ECommerceMicrosoftRepoBranch    = $d.data.ecommerceMicrosoftRepoBranch
                            }
                        }                             
                        New-Object PSObject -Property $hash | Select-PSFObject -TypeName "FSCPS.TOOLS.Versions" "*"
                }
            }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while getting the versionsData" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors"
            return
        }
        finally{
            
        }
    }
    END {
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        Installation of Nuget CLI
         
    .DESCRIPTION
        Download latest Nuget CLI
         
    .PARAMETER Path
        Download destination
         
    .PARAMETER Url
        Url/Uri to where the latest nuget download is located
         
        The default value is "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
         
    .EXAMPLE
        PS C:\> Install-FSCPSNugetCLI -Path "C:\temp\fscps.tools\nuget" -Url "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
         
        This will download the latest version of nuget.
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Install-FSCPSNugetCLI {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [string] $Path = "C:\temp\fscps.tools\nuget",
        [string] $Url = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
    )
    begin{
        $downloadPath = Join-Path -Path $Path -ChildPath "nuget.exe"

    if (-not (Test-PathExists -Path $Path -Type Container -Create)) { return }
    }
    process{
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Level Verbose -Message "Downloading nuget.exe. $($Url)" -Target $Url
        (New-Object System.Net.WebClient).DownloadFile($Url, $downloadPath)
    
        if (-not (Test-PathExists -Path $downloadPath -Type Leaf)) { return }
    }
    end{
        Unblock-File -Path $downloadPath
        Set-PSFConfig -FullName "fscps.tools.path.nuget" -Value $downloadPath
        Register-PSFConfig -FullName "fscps.tools.path.nuget"

        Update-ModuleVariables
    } 
}


<#
    .SYNOPSIS
        Function to sign the files with KeyVault
         
    .DESCRIPTION
        Function to sign the files with KeyVault
         
    .PARAMETER Uri
        A fully qualified URL of the key vault with the certificate that will be used for signing. An example value might be https://my-vault.vault.azure.net.
         
    .PARAMETER TenantId
        This is the tenant id used to authenticate to Azure, which will be used to generate an access token.
         
    .PARAMETER CertificateName
        The name of the certificate used to perform the signing operation.
         
    .PARAMETER ClientId
        This is the client ID used to authenticate to Azure, which will be used to generate an access token.
         
    .PARAMETER ClientSecret
        This is the client secret used to authenticate to Azure, which will be used to generate an access token.
         
    .PARAMETER TimestampServer
        A URL to an RFC3161 compliant timestamping service.
         
    .PARAMETER FILE
        A file to sign
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureSignToolSignFile -Uri "https://my-vault.vault.azure.net" `
        -TenantId "01234567-abcd-ef012-0000-0123456789ab" `
        -CertificateName "my-key-name" `
        -ClientId "01234567-abcd-ef012-0000-0123456789ab" `
        -ClientSecret "secret" `
        -FILE "$filePath"
         
        This will sign the target file with the KeyVault certificate
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Invoke-FSCPSAzureSignToolSignFile {
    param (
        [Parameter(HelpMessage = "A fully qualified URL of the key vault with the certificate that will be used for signing.", Mandatory = $false)]
        [string] $Uri,
        [Parameter(HelpMessage = "This is the tenant id used to authenticate to Azure, which will be used to generate an access token.", Mandatory = $true)]
        [string] $TenantId,
        [Parameter(HelpMessage = "The name of the certificate used to perform the signing operation.", Mandatory = $false)]
        [string] $CertificateName,
        [Parameter(HelpMessage = "This is the client ID used to authenticate to Azure, which will be used to generate an access token.", Mandatory = $false)]
        [string] $ClientId,
        [Parameter(HelpMessage = "This is the client secret used to authenticate to Azure, which will be used to generate an access token.", Mandatory = $true)]
        [SecureString] $ClientSecret,
        [Parameter(HelpMessage = "A URL to an RFC3161 compliant timestamping service.", Mandatory = $true)]
        [string] $TimestampServer = "http://timestamp.digicert.com",    
        [Parameter(HelpMessage = "A file to sign", Mandatory = $true)]
        [string] $FILE
    )
    begin{
        $tempDirectory = "c:\temp"
        if (!(Test-Path -Path $tempDirectory))
        {
            [System.IO.Directory]::CreateDirectory($tempDirectory)
        }
        
        if(-not (Test-Path $FILE ))
        {
            Write-Error "File $FILE is not found! Check the path."
            exit 1;
        }
        try {
            & dotnet tool install --global AzureSignTool;
        }
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while installing AzureSignTool" -Exception $PSItem.Exception
        }


    }
    process{
        try {
            & azuresigntool sign -kvu "$($Uri)" -kvt "$($TenantId)" -kvc "$($CertificateName)" -kvi "$($ClientId)" -kvs "$($ClientSecret)" -tr "$($TimestampServer)" -td sha256 "$FILE"
        }
        catch {
            
            Write-PSFMessage -Level Host -Message "Something went wrong while signing file. " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
    }
    end{

    }
}


<#
    .SYNOPSIS
        Delete a file to Azure
         
    .DESCRIPTION
        Delete any file to an Azure Storage Account
         
    .PARAMETER AccountId
        Storage Account Name / Storage Account Id where you want to store the file
         
    .PARAMETER AccessToken
        The token that has the needed permissions for the delete action
         
    .PARAMETER SAS
        The SAS key that you have created for the storage account or blob container
         
    .PARAMETER Container
        Name of the blob container inside the storage account you want to store the file
         
    .PARAMETER FileName
        Path to the file you want to delete
         
    .PARAMETER Force
        Instruct the cmdlet to overwrite the file in the container if it already exists
         
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions
        This is less user friendly, but allows catching exceptions in calling scripts
         
    .EXAMPLE
        PS C:\> $AzureParams = Get-FSCActiveAzureStorageConfig
        PS C:\> New-D365Bacpac | Invoke-FSCPSAzureStorageDelete @AzureParams
         
        This will get the current Azure Storage Account configuration details and use them as parameters to delete the file from Azure Storage Account.
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureStorageDelete -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -FileName "UAT_20180701.bacpac"
         
        This will delete the "UAT_20180701.bacpac" from the "backupfiles" container, inside the "miscfiles" Azure Storage Account.
        A SAS key is used to gain access to the container and deleteng the file.
         
    .NOTES
        Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Bacpac, Container
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
        The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages.
         
#>

function Invoke-FSCPSAzureStorageDelete {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false)]
        [string] $AccountId = $Script:AzureStorageAccountId,

        [Parameter(Mandatory = $false)]
        [string] $AccessToken = $Script:AzureStorageAccessToken,

        [Parameter(Mandatory = $false)]
        [string] $SAS = $Script:AzureStorageSAS,

        [Parameter(Mandatory = $false)]
        [Alias('Blob')]
        [Alias('Blobname')]
        [string] $Container = $Script:AzureStorageContainer,

        [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipeline = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'Pipeline', ValueFromPipelineByPropertyName = $true)]
        [Alias('File')]
        [string] $FileName,

        [switch] $Force,

        [switch] $EnableException
    )
    BEGIN {
        if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or
            ([string]::IsNullOrEmpty($Container)) -or
            (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) {
            Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved."
            Stop-PSFFunction -Message "Stopping because of missing parameters"
            return
        }
    }
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }

        Invoke-TimeSignal -Start
        try {

            if ([string]::IsNullOrEmpty($SAS)) {
                Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken"

                $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken
            }
            else {
                $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS)

                Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" -Target $conString                
                $storageContext = New-AzStorageContext -ConnectionString $conString
            }

            Write-PSFMessage -Level Verbose -Message "Start deleting the file from Azure"

            $files = Get-FSCPSAzureStorageFile -Name $FileName
            foreach($file in $files)
            {
                $null = Remove-AzStorageBlob -Blob $file.Name -Container $($Container.ToLower()) -Context $storageContext -Force:$Force
                Write-PSFMessage -Level Verbose -Message "The blob $($file.Name) succesfully deleted."
            }
            if(-not $files)
            {
                Write-PSFMessage -Level Verbose -Message "Files with filter '$($FileName)' were not found in the Storage Account."
            }
        }
        catch {
            $messageString = "Something went wrong while <c='em'>uploading</c> the file to Azure."
            Write-PSFMessage -Level Host -Message $messageString -Exception $PSItem.Exception -Target $FileName
            Stop-PSFFunction -Message "Stopping because of errors." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -ErrorRecord $_
            return
        }
        finally {
            Invoke-TimeSignal -End
        }
    }

    END { }
}


<#
    .SYNOPSIS
        Download a file to Azure
         
    .DESCRIPTION
        Download any file to an Azure Storage Account
         
    .PARAMETER AccountId
        Storage Account Name / Storage Account Id where you want to fetch the file from
         
    .PARAMETER AccessToken
        The token that has the needed permissions for the download action
         
    .PARAMETER SAS
        The SAS key that you have created for the storage account or blob container
         
    .PARAMETER Container
        Name of the blob container inside the storage account you where the file is
         
    .PARAMETER FileName
        Name of the file that you want to download
         
    .PARAMETER Path
        Path to the folder / location you want to save the file
         
        The default path is "c:\temp\fscps.tools"
         
    .PARAMETER Latest
        Instruct the cmdlet to download the latest file from Azure regardless of name
         
    .PARAMETER Force
        Instruct the cmdlet to overwrite the local file if it already exists
         
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions
        This is less user friendly, but allows catching exceptions in calling scripts
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureStorageDownload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -FileName "OriginalUAT.bacpac" -Path "c:\temp"
         
        Will download the "OriginalUAT.bacpac" file from the storage account and save it to "c:\temp\OriginalUAT.bacpac"
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureStorageDownload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Path "c:\temp" -Latest
         
        Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\".
        The complete path to the file will returned as output from the cmdlet.
         
    .EXAMPLE
        PS C:\> $AzureParams = Get-FSCPSActiveAzureStorageConfig
        PS C:\> Invoke-FSCPSAzureStorageDownload @AzureParams -Path "c:\temp" -Latest
         
        This will get the current Azure Storage Account configuration details
        and use them as parameters to download the latest file from an Azure Storage Account
         
        Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\".
        The complete path to the file will returned as output from the cmdlet.
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureStorageDownload -Latest
         
        This will use the default parameter values that are based on the configuration stored inside "Get-FSCPSActiveAzureStorageConfig".
        Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\fscps.tools".
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureStorageDownload -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Path "c:\temp" -Latest
         
        Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\".
        A SAS key is used to gain access to the container and downloading the file from it.
        The complete path to the file will returned as output from the cmdlet.
         
    .NOTES
        Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Latest, Bacpac, Container
         
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
        The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages.
         
#>

function Invoke-FSCPSAzureStorageDownload {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false)]
        [string] $AccountId = $Script:AzureStorageAccountId,

        [Parameter(Mandatory = $false)]
        [string] $AccessToken = $Script:AzureStorageAccessToken,

        [Parameter(Mandatory = $false)]
        [string] $SAS = $Script:AzureStorageSAS,

        [Alias('Blob')]
        [Alias('Blobname')]
        [string] $Container = $Script:AzureStorageContainer,

        [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [string] $FileName,

        [string] $Path = $Script:DefaultTempPath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Latest', Position = 4 )]
        [Alias('GetLatest')]
        [switch] $Latest,

        [switch] $Force,

        [switch] $EnableException
    )

    BEGIN {
        if (-not (Test-PathExists -Path $Path -Type Container -Create)) {
            return
        }

        if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or
            ([string]::IsNullOrEmpty($Container)) -or
            (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) {
            Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved."
            Stop-PSFFunction -Message "Stopping because of missing parameters"
            return
        }
    }
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }

        Invoke-TimeSignal -Start

        try {

            if ([string]::IsNullOrEmpty($SAS)) {
                Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken"

                $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken
            }
            else {
                Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS"

                $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS)
                $storageContext = New-AzStorageContext -ConnectionString $conString
            }

            Write-PSFMessage -Level Verbose -Message "Start download from Azure Storage Account"

            if ($Latest) {
                $files = Get-AzStorageBlob -Container $($Container.ToLower()) -Context $storageContext

                $File = ($files | Sort-Object -Descending { $_.LastModified } | Select-Object -First 1)

                $FileName = $File.Name
                
                Write-PSFMessage -Level Verbose -Message "Filename is: $FileName"

                $NewFile = Join-Path $Path $($File.Name)

                $null = Get-AzStorageBlobContent -Container $($Container.ToLower()) -Blob $File.Name -Destination $NewFile -Context $storageContext -Force:$Force
            }
            else {

                Write-PSFMessage -Level Verbose -Message "Filename is: $FileName"

                $NewFile = Join-Path $Path $FileName

                $null = Get-AzStorageBlobContent -Container $($Container.ToLower()) -Blob $FileName -Destination $NewFile -Context $storageContext -Force:$Force
            }

            Get-Item -Path $NewFile | Select-PSFObject "Name as Filename", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, "LastWriteTime as LastModified", "Fullname as File"
        }
        catch {
            $messageString = "Something went wrong while <c='em'>downloading</c> the file from Azure."
            Write-PSFMessage -Level Host -Message $messageString -Exception $PSItem.Exception -Target $NewFile
            Stop-PSFFunction -Message "Stopping because of errors." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -ErrorRecord $_
            return
        }
        finally {
            Invoke-TimeSignal -End
        }
    }

    END { }
}


<#
    .SYNOPSIS
        Upload a file to Azure
         
    .DESCRIPTION
        Upload any file to an Azure Storage Account
         
    .PARAMETER AccountId
        Storage Account Name / Storage Account Id where you want to store the file
         
    .PARAMETER AccessToken
        The token that has the needed permissions for the upload action
         
    .PARAMETER SAS
        The SAS key that you have created for the storage account or blob container
         
    .PARAMETER Container
        Name of the blob container inside the storage account you want to store the file
         
    .PARAMETER Filepath
        Path to the file you want to upload
         
    .PARAMETER ContentType
        Media type of the file that is going to be uploaded
         
        The value will be used for the blob property "Content Type".
        If the parameter is left empty, the commandlet will try to automatically determined the value based on the file's extension.
        If the parameter is left empty and the value cannot be automatically be determined, Azure storage will automatically assign "application/octet-stream" as the content type.
        Valid media type values can be found here: https://www.iana.org/assignments/media-types/media-types.xhtml
         
    .PARAMETER Force
        Instruct the cmdlet to overwrite the file in the container if it already exists
         
    .PARAMETER DeleteOnUpload
        Switch to tell the cmdlet if you want the local file to be deleted after the upload completes
         
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions
        This is less user friendly, but allows catching exceptions in calling scripts
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureStorageUpload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Filepath "c:\temp\bacpac\UAT_20180701.bacpac" -DeleteOnUpload
         
        This will upload the "c:\temp\bacpac\UAT_20180701.bacpac" up to the "backupfiles" container, inside the "miscfiles" Azure Storage Account that is access with the "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" token.
        After upload the local file will be deleted.
         
    .EXAMPLE
        PS C:\> $AzureParams = Get-D365ActiveAzureStorageConfig
        PS C:\> New-D365Bacpac | Invoke-FSCPSAzureStorageUpload @AzureParams
         
        This will get the current Azure Storage Account configuration details and use them as parameters to upload the file to an Azure Storage Account.
         
    .EXAMPLE
        PS C:\> New-D365Bacpac | Invoke-FSCPSAzureStorageUpload
         
        This will generate a new bacpac file using the "New-D365Bacpac" cmdlet.
        The file will be uploaded to an Azure Storage Account using the "Invoke-FSCPSAzureStorageUpload" cmdlet.
        This will use the default parameter values that are based on the configuration stored inside "Get-D365ActiveAzureStorageConfig" for the "Invoke-FSCPSAzureStorageUpload" cmdlet.
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSAzureStorageUpload -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Filepath "c:\temp\bacpac\UAT_20180701.bacpac"
         
        This will upload the "c:\temp\bacpac\UAT_20180701.bacpac" up to the "backupfiles" container, inside the "miscfiles" Azure Storage Account.
        A SAS key is used to gain access to the container and uploading the file to it.
         
    .NOTES
        Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Bacpac, Container
         
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
        The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages.
         
#>

function Invoke-FSCPSAzureStorageUpload {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false)]
        [string] $AccountId = $Script:AzureStorageAccountId,

        [Parameter(Mandatory = $false)]
        [string] $AccessToken = $Script:AzureStorageAccessToken,

        [Parameter(Mandatory = $false)]
        [string] $SAS = $Script:AzureStorageSAS,

        [Parameter(Mandatory = $false)]
        [Alias('Blob')]
        [Alias('Blobname')]
        [string] $Container = $Script:AzureStorageContainer,

        [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipeline = $true)]
        [Parameter(Mandatory = $true, ParameterSetName = 'Pipeline', ValueFromPipelineByPropertyName = $true)]
        [Alias('File')]
        [Alias('Path')]
        [string] $Filepath,

        [Parameter(Mandatory = $false)]
        [string] $ContentType,

        [switch] $Force,
        [Parameter(Mandatory = $false)]
        [switch] $DeleteOnUpload,

        [switch] $EnableException
    )
    BEGIN {
        if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or
            ([string]::IsNullOrEmpty($Container)) -or
            (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) {
            Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved."
            Stop-PSFFunction -Message "Stopping because of missing parameters"
            return
        }
    }
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }

        Invoke-TimeSignal -Start

        $FileName = Split-Path -Path $Filepath -Leaf
        try {

            if ([string]::IsNullOrEmpty($SAS)) {
                Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken"

                $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken
            }
            else {
                $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS)

                Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" -Target $conString
                
                $storageContext = New-AzStorageContext -ConnectionString $conString
            }

            Write-PSFMessage -Level Verbose -Message "Start uploading the file to Azure"

            if ([string]::IsNullOrEmpty($ContentType)) {
                $ContentType = Get-MediaTypeByFilename $FileName # Available since .NET4.5, so it can be used with PowerShell 5.0 and higher.

                Write-PSFMessage -Level Verbose -Message "Content Type is automatically set to value: $ContentType"
            }

            $null = Set-AzStorageBlobContent -Context $storageContext -File $Filepath -Container $($Container.ToLower()) -Properties @{"ContentType" = $ContentType} -Force:$Force

            if ($DeleteOnUpload) {
                Remove-Item $Filepath -Force
            }

            [PSCustomObject]@{
                File     = $Filepath
                Filename = $FileName
            }
        }
        catch {
            $messageString = "Something went wrong while <c='em'>uploading</c> the file to Azure."
            Write-PSFMessage -Level Host -Message $messageString -Exception $PSItem.Exception -Target $FileName
            Stop-PSFFunction -Message "Stopping because of errors." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -ErrorRecord $_
            return
        }
        finally {
            Invoke-TimeSignal -End
        }
    }

    END { }
}


<#
    .SYNOPSIS
        Install software from Choco
         
    .DESCRIPTION
        Installs software from Chocolatey
         
        Full list of software: https://community.chocolatey.org/packages
         
    .PARAMETER Command
        The command of the choco to execute
         
        Support a list of softwares that you want to have installed on the system
         
    .PARAMETER Silent
        Disable output
         
    .PARAMETER SkipUpdate
        Skip the chocolatey update
         
    .PARAMETER Command
        The command of the choco to execute
         
    .PARAMETER RemainingArguments
        List of arguments
         
    .PARAMETER Force
        Force command. Reinstall latest version if command is install or upgrade to latest version
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSChoco install gh -y --allow-unofficial -Silent
         
        This will install GH tools on the system without console output
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


Function Invoke-FSCPSChoco {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $Command,
        [Parameter(Mandatory = $false, Position = 1, ValueFromRemainingArguments = $true)] $RemainingArguments,
        [switch] $Silent,
        [switch] $SkipUpdate,
        [switch] $Force
    )

    BEGIN {
        Invoke-TimeSignal -Start
        try {
            if (Test-Path -Path "$env:ProgramData\Chocolatey") {
                if($SkipUpdate)
                {
                    if (!$Silent) {
                        choco upgrade chocolatey -y -r 
                        choco upgrade all --ignore-checksums -y -r
                    }
                    else{
                        $null = choco upgrade chocolatey -y -r -silent
                        $null = choco upgrade all --ignore-checksums -y -r
                    }
                }
            }
            else {
                Write-PSFMessage -Level InternalComment -Message "Installing Chocolatey"
            
                # Download and execute installation script
                [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials
                Invoke-Expression ((New-Object System.Net.WebClient).DownloadString("https://chocolatey.org/install.ps1"))
            }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while installing or updating Chocolatey" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors"
            return
        }

        #Determine choco executable location
        # This is needed because the path variable is not updated in this session yet
        # This part is copied from https://chocolatey.org/install.ps1
        $chocoPath = [Environment]::GetEnvironmentVariable("ChocolateyInstall")
        if ($chocoPath -eq $null -or $chocoPath -eq '') {
            $chocoPath = "$env:ALLUSERSPROFILE\Chocolatey"
        }
        if (!(Test-Path ($chocoPath))) {
            $chocoPath = "$env:SYSTEMDRIVE\ProgramData\Chocolatey"
        }
        $chocoExePath = Join-Path $chocoPath 'bin\choco.exe'

        if (-not (Test-PathExists -Path $chocoExePath -Type Leaf)) { return }

    }
    
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }
        
        try {
            foreach ($item in $Name) {
                Write-PSFMessage -Level InternalComment -Message "Installing $item"

                $arguments = New-Object System.Collections.Generic.List[System.Object]


                $arguments.Add("$Command ")
                $RemainingArguments | ForEach-Object {
                    if ("$_".IndexOf(" ") -ge 0 -or "$_".IndexOf('"') -ge 0) {
                        $arguments.Add("""$($_.Replace('"','\"'))"" ")
                    }
                    else {
                        $arguments.Add("$_ ")
                    }
                }
                if ($Force) {
                    $arguments.Add("-f")
                }
                if (!$Silent) {
                    Invoke-Process -Executable $chocoExePath -Params $($arguments.ToArray()) -ShowOriginalProgress:$true
                }
                else {
                    $null = Invoke-Process -Executable $chocoExePath -Params $($arguments.ToArray()) -ShowOriginalProgress:$false
                }
            }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while installing software" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors"
            return
        }
        finally{
            
        }
    }
    END {
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        Invoke the D365FSC models compilation
         
    .DESCRIPTION
        Invoke the D365FSC models compilation
         
    .PARAMETER Version
        The version of the D365FSC used to build
         
    .PARAMETER Type
        The type of the FSCPS project to build
         
    .PARAMETER SourcesPath
        The folder contains a metadata files with binaries
         
    .PARAMETER BuildFolderPath
        The destination build folder
         
    .PARAMETER OutputAsHashtable
        Instruct the cmdlet to return a hashtable object
         
    .PARAMETER Force
        Cleanup destination build folder befor build
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSCompile -Version "10.0.39" -Type FSCM
         
        Example output:
         
        METADATA_DIRECTORY : D:\a\8\s\Metadata
        FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99
        BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin
        NUGETS_FOLDER : C:\temp\buildbuild\packages
        BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log
        PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78
        PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip
        ARTIFACTS_PATH : C:\temp\buildbuild\artifacts
        ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"]
         
        This will build D365FSC package with version "10.0.39" to the Temp folder
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSCompile -Version "10.0.39" -Path "c:\Temp"
         
        Example output:
         
        METADATA_DIRECTORY : D:\a\8\s\Metadata
        FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99
        BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin
        NUGETS_FOLDER : C:\temp\buildbuild\packages
        BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log
        PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78
        PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip
        ARTIFACTS_PATH : C:\temp\buildbuild\artifacts
        ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"]
         
        This will build D365FSC package with version "10.0.39" to the Temp folder
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Invoke-FSCPSCompile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")]
    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param (
        [string] $Version,
        [Parameter(Mandatory = $true)]
        [string] $SourcesPath,
        [FSCPSType]$Type,
        [string] $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld),
        [switch] $OutputAsHashtable,        
        [switch] $Force
    )

    BEGIN {
        Invoke-TimeSignal -Start
        try {            
            $settings = Get-FSCPSSettings -OutputAsHashtable 
            $responseObject = [Ordered]@{}

            if($settings.type -eq '' -and ($null -eq $Type))
            {
                throw "Project type should be provided!"
            }

            if($settings.type -eq '')
            {
                $settings.type = $Type
            }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while compiling " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }

    }
    
    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }
        try {            
            switch($settings.type)
            {
                'FSCM' { 
                    $responseObject = (Invoke-FSCCompile -Version $Version -SourcesPath $SourcesPath -BuildFolderPath $BuildFolderPath -Force:$Force )
                    break;
                }
                'ECommerce' { 
                    #$responseObject = (Invoke-ECommerceCompile -Version $Version -SourcesPath $SourcesPath -BuildFolderPath $BuildFolderPath -Force:$Force)
                    #break;
                }
                'Commerce' { 
                    $responseObject = (Invoke-CommerceCompile -Version $Version -SourcesPath $SourcesPath -BuildFolderPath $BuildFolderPath -Force:$Force) 
                    break;
                }
                Default{
                    throw "Project type should be provided!"
                }
            }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Something went wrong while compiling " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
        finally{
            if($OutputAsHashtable) {
                $responseObject
            } else {
                [PSCustomObject]$responseObject
            }   
        }
    }
    END {
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        Function to sign the files with digicert
         
    .DESCRIPTION
        Function to sign the files with digicert
         
    .PARAMETER SM_HOST
        Digicert host URL. Default value "https://clientauth.one.digicert.com"
         
    .PARAMETER SM_API_KEY
        The DigiCert API Key
         
    .PARAMETER SM_CLIENT_CERT_FILE
        The DigiCert certificate local path (p12)
         
    .PARAMETER SM_CLIENT_CERT_FILE_URL
        The DigiCert certificate URL (p12)
         
    .PARAMETER SM_CLIENT_CERT_PASSWORD
        The DigiCert certificate password
         
    .PARAMETER SM_CODE_SIGNING_CERT_SHA1_HASH
        The DigiCert certificate thumbprint(fingerprint)
         
    .PARAMETER FILE
        A file to sign
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSDigiCertSignFile -SM_API_KEY "$codeSignDigiCertAPISecretName" `
        -SM_CLIENT_CERT_FILE_URL "$codeSignDigiCertUrlSecretName" `
        -SM_CLIENT_CERT_PASSWORD $(ConvertTo-SecureString $codeSignDigiCertPasswordSecretName -AsPlainText -Force) `
        -SM_CODE_SIGNING_CERT_SHA1_HASH "$codeSignDigiCertHashSecretName" `
        -FILE "$filePath"
         
        This will sign the target file with the DigiCert certificate
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Invoke-FSCPSDigiCertSignFile {
    param (
        [Parameter(HelpMessage = "The DigiCert host", Mandatory = $false)]
        [string] $SM_HOST = "https://clientauth.one.digicert.com",
        [Parameter(HelpMessage = "The DigiCert API Key", Mandatory = $true)]
        [string] $SM_API_KEY,
        [Parameter(HelpMessage = "The DigiCert certificate local path (p12)", Mandatory = $false)]
        [string] $SM_CLIENT_CERT_FILE = "c:\temp\digicert.p12",
        [Parameter(HelpMessage = "The DigiCert certificate URL (p12)", Mandatory = $false)]
        [string] $SM_CLIENT_CERT_FILE_URL,
        [Parameter(HelpMessage = "The DigiCert certificate password", Mandatory = $true)]
        [SecureString] $SM_CLIENT_CERT_PASSWORD,
        [Parameter(HelpMessage = "The DigiCert certificate thumbprint(fingerprint)", Mandatory = $true)]
        [string] $SM_CODE_SIGNING_CERT_SHA1_HASH,    
        [Parameter(HelpMessage = "A file to sign", Mandatory = $true)]
        [string] $FILE
    )
    begin{
        $tempDirectory = "c:\temp"
        if (!(Test-Path -Path $tempDirectory))
        {
            [System.IO.Directory]::CreateDirectory($tempDirectory)
        }
        $certLocation = "$tempDirectory\digicert.p12"
        if(-not (Test-Path $FILE ))
        {
            Write-Error "File $FILE is not found! Check the path."
            exit 1;
        }
        if(-not (Test-Path $SM_CLIENT_CERT_FILE ))
        {            
            if(![string]::IsNullOrEmpty($SM_CLIENT_CERT_FILE_URL))
            {
                $certLocation = Join-Path $tempDirectory "digiCert.p12"
                Invoke-WebRequest -Uri "$SM_CLIENT_CERT_FILE_URL" -OutFile $certLocation
                if(Test-Path $certLocation)
                {
                    $SM_CLIENT_CERT_FILE = $certLocation
                }
            }

            if(-not (Test-Path $SM_CLIENT_CERT_FILE ))
            {
                Write-Error "Certificate $SM_CLIENT_CERT_FILE is not found! Check the path."
                exit 1;
            }
        }

        $currentLocation = Get-Location
        $signMessage = ""
        #set env variables
        $env:SM_CLIENT_CERT_FILE = $SM_CLIENT_CERT_FILE
        $env:SM_HOST = $SM_HOST 
        $env:SM_API_KEY = $SM_API_KEY
        $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SM_CLIENT_CERT_PASSWORD)
        $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
        $env:SM_CLIENT_CERT_PASSWORD = $UnsecurePassword       
        Set-Location $tempDirectory
        if(-not (Test-Path -Path .\smtools-windows-x64.msi ))
        {
            Write-Output "===============smtools-windows-x64.msi================"
            $smtools = "smtools-windows-x64.msi"
            Write-Output "The '$smtools' not found. Downloading..."
            Invoke-WebRequest -Method Get https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -Headers @{ "x-api-key" = "$($SM_API_KEY)"}  -OutFile .\$smtools -Verbose
            Write-Output "Downloaded. Installing..."
            msiexec /i $smtools /quiet /qn /le smtools.log
            Get-Content smtools.log -ErrorAction SilentlyContinue
            Write-Output "Installed."
            Start-Sleep -Seconds 5
        }
        Write-Output "Checking DigiCert location..."
        $smctlLocation = (Get-ChildItem "$Env:Programfiles\DigiCert" -Recurse | Where-Object { $_.BaseName -like "smctl" })
        
        if(Test-Path $smctlLocation.FullName)
        {
            Write-Output "DigiCert directory found at: $($smctlLocation.Directory)"
        }
        else
        {
            Write-Error "DigiCert directory not found. Check the installation."
            exit 1
        }
        $appCertKitPath = "${env:ProgramFiles(x86)}\Windows Kits\10\App Certification Kit" 
        Set-PathVariable -Scope Process -RemovePath $appCertKitPath -ErrorAction SilentlyContinue
        Set-PathVariable -Scope Process -AddPath  $appCertKitPath -ErrorAction SilentlyContinue

        & certutil.exe -csp "DigiCert Software Trust Manager KSP" -key -user

        & $($smctlLocation.FullName) windows certsync

    }
    process{
        try {     
            try {
                if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){
                    Write-Output "===============Healthcheck================"
                    & $($smctlLocation.FullName) healthcheck
                    Write-Output "===============KeyPair list================"
                    & $($smctlLocation.FullName) keypair ls 
                }  
            }
            catch {   
                Write-Output "Healchcheck failed. please check it"
            }  

            Write-Output "Set-Location of DigiCert" 
            Set-Location $($smctlLocation.Directory)

            $signMessage = $(& $($smctlLocation.FullName) sign --fingerprint $SM_CODE_SIGNING_CERT_SHA1_HASH --input $FILE --verbose)
            Write-Output $($signMessage)
            if($signMessage.Contains("FAILED")){
                Write-Output (Get-Content "$env:USERPROFILE\.signingmanager\logs\smctl.log" -ErrorAction SilentlyContinue)
                throw;
            }
            if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){
                & $($smctlLocation.FullName) sign verify --input $FILE
            }        
            
            Write-Output "File '$($FILE)' was signed successful!"
        }
        catch {
            
            Write-Output "Something went wrong! Read the healthcheck."
           # & $smctlLocation.FullName healthcheck
        }
    }
    end{
        Clear-Content $env:SM_HOST -Force -ErrorAction SilentlyContinue
        Clear-Content $env:SM_API_KEY -Force -ErrorAction SilentlyContinue
        Clear-Content $env:SM_CLIENT_CERT_PASSWORD -Force -ErrorAction SilentlyContinue
        Set-Location $currentLocation
        if((Test-Path $certLocation ))
        {  
            Remove-Item $certLocation -Force -ErrorAction SilentlyContinue
        }
    }
}


<#
    .SYNOPSIS
        Function to sign the files with digicert
         
    .DESCRIPTION
        Function to sign the files with digicert
         
    .PARAMETER SM_HOST
        Digicert host URL. Default value "https://clientauth.one.digicert.com"
         
    .PARAMETER SM_API_KEY
        The DigiCert API Key
         
    .PARAMETER SM_CLIENT_CERT_FILE
        The DigiCert certificate local path (p12)
         
    .PARAMETER SM_CLIENT_CERT_FILE_URL
        The DigiCert certificate URL (p12)
         
    .PARAMETER SM_CLIENT_CERT_PASSWORD
        The DigiCert certificate password
         
    .PARAMETER SM_CODE_SIGNING_CERT_SHA1_HASH
        The DigiCert certificate thumbprint(fingerprint)
         
    .PARAMETER FILE
        A file to sign
         
    .EXAMPLE
        PS C:\> Invoke-FSCPSSignBinaryFile -SM_API_KEY "$codeSignDigiCertAPISecretName" `
        -SM_CLIENT_CERT_FILE_URL "$codeSignDigiCertUrlSecretName" `
        -SM_CLIENT_CERT_PASSWORD $(ConvertTo-SecureString $codeSignDigiCertPasswordSecretName -AsPlainText -Force) `
        -SM_CODE_SIGNING_CERT_SHA1_HASH "$codeSignDigiCertHashSecretName" `
        -FILE "$filePath"
         
        This will sign the target file with the DigiCert certificate
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Invoke-FSCPSSignBinaryFile {
    param (
        [Parameter(HelpMessage = "The DigiCert host", Mandatory = $false)]
        [string] $SM_HOST = "https://clientauth.one.digicert.com",
        [Parameter(HelpMessage = "The DigiCert API Key", Mandatory = $true)]
        [string] $SM_API_KEY,
        [Parameter(HelpMessage = "The DigiCert certificate local path (p12)", Mandatory = $false)]
        [string] $SM_CLIENT_CERT_FILE = "c:\temp\digicert.p12",
        [Parameter(HelpMessage = "The DigiCert certificate URL (p12)", Mandatory = $false)]
        [string] $SM_CLIENT_CERT_FILE_URL,
        [Parameter(HelpMessage = "The DigiCert certificate password", Mandatory = $true)]
        [SecureString] $SM_CLIENT_CERT_PASSWORD,
        [Parameter(HelpMessage = "The DigiCert certificate thumbprint(fingerprint)", Mandatory = $true)]
        [string] $SM_CODE_SIGNING_CERT_SHA1_HASH,    
        [Parameter(HelpMessage = "A file to sign", Mandatory = $true)]
        [string] $FILE
    )
    begin{
        $tempDirectory = "c:\temp"
        if (!(Test-Path -Path $tempDirectory))
        {
            [System.IO.Directory]::CreateDirectory($tempDirectory)
        }
        $certLocation = "$tempDirectory\digicert.p12"
        if(-not (Test-Path $FILE ))
        {
            Write-Error "File $FILE is not found! Check the path."
            exit 1;
        }
        if(-not (Test-Path $SM_CLIENT_CERT_FILE ))
        {            
            if(![string]::IsNullOrEmpty($SM_CLIENT_CERT_FILE_URL))
            {
                $certLocation = Join-Path $tempDirectory "digiCert.p12"
                Invoke-WebRequest -Uri "$SM_CLIENT_CERT_FILE_URL" -OutFile $certLocation
                if(Test-Path $certLocation)
                {
                    $SM_CLIENT_CERT_FILE = $certLocation
                }
            }

            if(-not (Test-Path $SM_CLIENT_CERT_FILE ))
            {
                Write-Error "Certificate $SM_CLIENT_CERT_FILE is not found! Check the path."
                exit 1;
            }
        }

        $currentLocation = Get-Location
        $signMessage = ""
        #set env variables
        $env:SM_CLIENT_CERT_FILE = $SM_CLIENT_CERT_FILE
        $env:SM_HOST = $SM_HOST 
        $env:SM_API_KEY = $SM_API_KEY
        $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SM_CLIENT_CERT_PASSWORD)
        $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
        $env:SM_CLIENT_CERT_PASSWORD = $UnsecurePassword       
        Set-Location $tempDirectory
        if(-not (Test-Path -Path .\smtools-windows-x64.msi ))
        {
            Write-Output "===============smtools-windows-x64.msi================"
            $smtools = "smtools-windows-x64.msi"
            Write-Output "The '$smtools' not found. Downloading..."
            Invoke-WebRequest -Method Get https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -Headers @{ "x-api-key" = "$($SM_API_KEY)"}  -OutFile .\$smtools -Verbose
            Write-Output "Downloaded. Installing..."
            msiexec /i $smtools /quiet /qn /le smtools.log
            Get-Content smtools.log -ErrorAction SilentlyContinue
            Write-Output "Installed."
            Start-Sleep -Seconds 5
        }
        Write-Output "Checking DigiCert location..."
        $smctlLocation = (Get-ChildItem "$Env:Programfiles\DigiCert" -Recurse | Where-Object { $_.BaseName -like "smctl" })
        
        if(Test-Path $smctlLocation.FullName)
        {
            Write-Output "DigiCert directory found at: $($smctlLocation.Directory)"
        }
        else
        {
            Write-Error "DigiCert directory not found. Check the installation."
            exit 1
        }
        $appCertKitPath = "${env:ProgramFiles(x86)}\Windows Kits\10\App Certification Kit" 
        Set-PathVariable -Scope Process -RemovePath $appCertKitPath -ErrorAction SilentlyContinue
        Set-PathVariable -Scope Process -AddPath  $appCertKitPath -ErrorAction SilentlyContinue

        & certutil.exe -csp "DigiCert Software Trust Manager KSP" -key -user

        & $($smctlLocation.FullName) windows certsync

    }
    process{
        try {     
            try {
                if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){
                    Write-Output "===============Healthcheck================"
                    & $($smctlLocation.FullName) healthcheck
                    Write-Output "===============KeyPair list================"
                    & $($smctlLocation.FullName) keypair ls 
                }  
            }
            catch {   
                Write-Output "Healchcheck failed. please check it"
            }  

            Write-Output "Set-Location of DigiCert" 
            Set-Location $($smctlLocation.Directory)

            $signMessage = $(& $($smctlLocation.FullName) sign --fingerprint $SM_CODE_SIGNING_CERT_SHA1_HASH --input $FILE --verbose)
            Write-Output $($signMessage)
            if($signMessage.Contains("FAILED")){
                Write-Output (Get-Content "$env:USERPROFILE\.signingmanager\logs\smctl.log" -ErrorAction SilentlyContinue)
                throw;
            }
            if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){
                & $($smctlLocation.FullName) sign verify --input $FILE
            }        
            
            Write-Output "File '$($FILE)' was signed successful!"
        }
        catch {
            
            Write-Output "Something went wrong! Read the healthcheck."
           # & $smctlLocation.FullName healthcheck
        }
    }
    end{
        Clear-Content $env:SM_HOST -Force -ErrorAction SilentlyContinue
        Clear-Content $env:SM_API_KEY -Force -ErrorAction SilentlyContinue
        Clear-Content $env:SM_CLIENT_CERT_PASSWORD -Force -ErrorAction SilentlyContinue
        Set-Location $currentLocation
        if((Test-Path $certLocation ))
        {  
            Remove-Item $certLocation -Force -ErrorAction SilentlyContinue
        }
    }
}


<#
    .SYNOPSIS
        Register Azure Storage Configurations
         
    .DESCRIPTION
        Register all Azure Storage Configurations
         
    .PARAMETER ConfigStorageLocation
        Parameter used to instruct where to store the configuration objects
         
        The default value is "User" and this will store all configuration for the active user
         
        Valid options are:
        "User"
        "System"
         
        "System" will store the configuration as default for all users, so they can access the configuration objects
         
    .EXAMPLE
        PS C:\> Register-FSCPSAzureStorageConfig -ConfigStorageLocation "System"
         
        This will store all Azure Storage Configurations as defaults for all users on the machine.
         
    .NOTES
        Tags: Configuration, Azure, Storage
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Register-FSCPSAzureStorageConfig {
    [CmdletBinding()]
    [OutputType()]
    param (
        [ValidateSet('User', 'System')]
        [string] $ConfigStorageLocation = "User"
    )

    $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation
    
    Register-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Scope $configScope
}


<#
    .SYNOPSIS
        Set the active Azure Storage Account configuration
         
    .DESCRIPTION
        Updates the current active Azure Storage Account configuration with a new one
         
    .PARAMETER Name
        The name the Azure Storage Account configuration you want to load into the active Azure Storage Account configuration
         
    .PARAMETER ConfigStorageLocation
        Parameter used to instruct where to store the configuration objects
         
        The default value is "User" and this will store all configuration for the active user
         
        Valid options are:
        "User"
        "System"
         
        "System" will store the configuration so all users can access the configuration objects
         
    .PARAMETER Temporary
        Instruct the cmdlet to only temporarily override the persisted settings in the configuration storage
         
    .EXAMPLE
        PS C:\> Set-FSCPSActiveAzureStorageConfig -Name "UAT-Exports"
         
        This will import the "UAT-Exports" set from the Azure Storage Account configurations.
        It will update the active Azure Storage Account configuration.
         
    .EXAMPLE
        PS C:\> Set-FSCPSActiveAzureStorageConfig -Name "UAT-Exports" -ConfigStorageLocation "System"
         
        This will import the "UAT-Exports" set from the Azure Storage Account configurations.
        It will update the active Azure Storage Account configuration.
        The data will be stored in the system wide configuration storage, which makes it accessible from all users.
         
    .EXAMPLE
        PS C:\> Set-FSCPSActiveAzureStorageConfig -Name "UAT-Exports" -Temporary
         
        This will import the "UAT-Exports" set from the Azure Storage Account configurations.
        It will update the active Azure Storage Account configuration.
        The update will only last for the rest of this PowerShell console session.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
        You will have to run the Add-FSCPSAzureStorageConfig cmdlet at least once, before this will be capable of working.
         
#>

function Set-FSCPSActiveAzureStorageConfig {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [string] $Name,

        [ValidateSet('User', 'System')]
        [string] $ConfigStorageLocation = "User",
        
        [switch] $Temporary
    )

    $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation

    if (Test-PSFFunctionInterrupt) { return }

    $azureStorageConfigs = [hashtable] (Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts")

    if(!$azureStorageConfigs)
    {
        Init-AzureStorageDefault
        $azureStorageConfigs = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts")
    }

    if (-not ($azureStorageConfigs.ContainsKey($Name))) {
        Write-PSFMessage -Level Host -Message "An Azure Storage Account with that name <c='$Name'> doesn't exists</c>."
        Stop-PSFFunction -Message "Stopping because an Azure Storage Account with that name doesn't exists."
        return
    }
    else {
        $azureDetails = $azureStorageConfigs[$Name]

        Set-PSFConfig -FullName "fscps.tools.active.azure.storage.account" -Value $azureDetails
        if (-not $Temporary) { Register-PSFConfig -FullName "fscps.tools.active.azure.storage.account"  -Scope $configScope }
        
        Update-AzureStorageVariables
    }
}


<#
    .SYNOPSIS
        Set the FSCPS configuration details
         
    .DESCRIPTION
        Set the FSCPS configuration details from the configuration store
         
        All settings retrieved from this cmdlets is to be considered the default parameter values across the different cmdlets
         
    .PARAMETER SettingsJsonString
        String contains JSON with custom settings
         
    .PARAMETER SettingsFilePath
        Set path to the settings.json file
         
    .EXAMPLE
        PS C:\> Set-FSCPSSettings -SettingsFilePath "c:\temp\settings.json"
         
        This will output the current FSCPS configuration.
        The object returned will be a Hashtable.
         
    .LINK
        Get-FSCPSSettings
         
    .NOTES
        Tags: Environment, Url, Config, Configuration, Upload, ClientId, Settings
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
         
#>


function Set-FSCPSSettings {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param (
        [string] $SettingsFilePath,
        [string] $SettingsJsonString
    )
    begin{
        if((-not ($SettingsJsonString -eq "")) -and (-not ($SettingsFilePath -eq "")))
        {
            throw "Both settings parameters cannot be provided. Please provide only one of them."
        }

        if(-not ($SettingsJsonString -eq ""))
        {
            $SettingsFilePath = "C:\temp\settings.json"
            $null = Test-PathExists -Path "C:\temp\" -Type Container -Create
            $null = Set-Content $SettingsFilePath $SettingsJsonString -Force -PassThru
        }

        $fscpsFolderName = Get-PSFConfigValue -FullName "fscps.tools.settings.all.fscpsFolder"
        $fscmSettingsFile = Get-PSFConfigValue -FullName "fscps.tools.settings.all.fscpsSettingsFile"
        $fscmRepoSettingsFile = Get-PSFConfigValue -FullName "fscps.tools.settings.all.fscpsRepoSettingsFile"
        Write-PSFMessage -Level Verbose -Message "fscpsFolderName is: $fscpsFolderName"
        Write-PSFMessage -Level Verbose -Message "fscmSettingsFile is: $fscmSettingsFile"
        Write-PSFMessage -Level Verbose -Message "fscmRepoSettingsFile is: $fscmRepoSettingsFile"
        $settingsFiles = @()
        $res = [Ordered]@{}

        $reposytoryName = ""
        $reposytoryOwner = ""
        $currentBranchName = ""

        
        if($Script:IsOnGitHub)# If GitHub context
        {
            Write-PSFMessage -Level Important -Message "Running on GitHub"
            Set-PSFConfig -FullName 'fscps.tools.settings.all.repoProvider' -Value 'GitHub'
            Set-PSFConfig -FullName 'fscps.tools.settings.all.repositoryRootPath' -Value "$env:GITHUB_WORKSPACE"            

            Set-PSFConfig -FullName 'fscps.tools.settings.all.runId' -Value "$ENV:GITHUB_RUN_NUMBER"
            Set-PSFConfig -FullName 'fscps.tools.settings.all.workflowName' -Value "$ENV:GITHUB_WORKFLOW"

            if($SettingsFilePath -eq "")
            {
                $RepositoryRootPath = "$env:GITHUB_WORKSPACE"
                Write-PSFMessage -Level Verbose -Message "GITHUB_WORKSPACE is: $RepositoryRootPath"
                
                $settingsFiles += (Join-Path $fscpsFolderName $fscmSettingsFile)
            }
            else{
                $settingsFiles += $SettingsFilePath
            }

            $reposytoryOwner = "$env:GITHUB_REPOSITORY".Split("/")[0]
            $reposytoryName = "$env:GITHUB_REPOSITORY".Split("/")[1]
            Write-PSFMessage -Level Verbose -Message "GITHUB_REPOSITORY is: $reposytoryName"
            $branchName = "$env:GITHUB_REF"
            Write-PSFMessage -Level Verbose -Message "GITHUB_REF is: $branchName"
            $currentBranchName = [regex]::Replace($branchName.Replace("refs/heads/","").Replace("/","_"), '(?i)(?:^|-|_)(\p{L})', { $args[0].Groups[1].Value.ToUpper()})      
            $gitHubFolder = ".github"

            $workflowName = "$env:GITHUB_WORKFLOW"
            Write-PSFMessage -Level Verbose -Message "GITHUB_WORKFLOW is: $workflowName"
            $workflowName = ($workflowName.Split([System.IO.Path]::getInvalidFileNameChars()) -join "").Replace("(", "").Replace(")", "").Replace("/", "")

            $settingsFiles += (Join-Path $gitHubFolder $fscmRepoSettingsFile)            
            $settingsFiles += (Join-Path $gitHubFolder "$workflowName.settings.json")
            
        }
        elseif($Script:IsOnAzureDevOps)# If Azure DevOps context
        {
            Write-PSFMessage -Level Verbose -Message "Running on Azure"
            Set-PSFConfig -FullName 'fscps.tools.settings.all.repoProvider' -Value 'AzureDevOps'
            Set-PSFConfig -FullName 'fscps.tools.settings.all.repositoryRootPath' -Value "$env:PIPELINE_WORKSPACE"
            Set-PSFConfig -FullName 'fscps.tools.settings.all.runId' -Value "$ENV:Build_BuildNumber"
            Set-PSFConfig -FullName 'fscps.tools.settings.all.workflowName' -Value "$ENV:Build_DefinitionName" 
            if($SettingsFilePath -eq "")
            {
                $RepositoryRootPath = "$env:PIPELINE_WORKSPACE"
                Write-PSFMessage -Level Verbose -Message "RepositoryRootPath is: $RepositoryRootPath"
            }
            else{
                $settingsFiles += $SettingsFilePath
            }
            
            $reposytoryOwner = $($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI.replace('https://dev.azure.com/', '').replace('/', '').replace('https:',''))
            $reposytoryName = "$env:SYSTEM_TEAMPROJECT"
            $branchName = "$env:BUILD_SOURCEBRANCH"
            $currentBranchName = [regex]::Replace($branchName.Replace("/Metadata","").Replace("$/$($reposytoryName)/","").Replace("$/$($reposytoryName)","").Replace("Trunk/","").Replace("/","_"), '(?i)(?:^|-|_)(\p{L})', { $args[0].Groups[1].Value.ToUpper() })   

            #$settingsFiles += (Join-Path $fscpsFolderName $fscmSettingsFile)

        }
        else { # If Desktop or other
            Write-PSFMessage -Level Verbose -Message "Running on desktop"
            Set-PSFConfig -FullName 'fscps.tools.settings.all.repoProvider' -Value 'Other'
            if($SettingsFilePath -eq "")
            {
                throw "SettingsFilePath variable should be passed if running on the cloud/personal computer"
            }
            $reposytoryName = "windows host"
            Set-PSFConfig -FullName 'fscps.tools.settings.all.runId' -Value 1
            $currentBranchName = 'DEV'
            $settingsFiles += $SettingsFilePath
        }

        Set-PSFConfig -FullName 'fscps.tools.settings.all.currentBranch' -Value $currentBranchName
        Set-PSFConfig -FullName 'fscps.tools.settings.all.repoOwner' -Value $reposytoryOwner
        Set-PSFConfig -FullName 'fscps.tools.settings.all.repoName' -Value $reposytoryName

        
        function MergeCustomObjectIntoOrderedDictionary {
            Param(
                [System.Collections.Specialized.OrderedDictionary] $dst,
                [PSCustomObject] $src
            )
        
            # Add missing properties in OrderedDictionary

            $src.PSObject.Properties.GetEnumerator() | ForEach-Object {
                $prop = $_.Name
                $srcProp = $src."$prop"
                $srcPropType = $srcProp.GetType().Name
                if (-not $dst.Contains($prop)) {
                    if ($srcPropType -eq "PSCustomObject") {
                        $dst.Add("$prop", [ordered]@{})
                    }
                    elseif ($srcPropType -eq "Object[]") {
                        $dst.Add("$prop", @())
                    }
                    else {
                        $dst.Add("$prop", $srcProp)
                    }
                }
            }
        
            @($dst.Keys) | ForEach-Object {
                $prop = $_
                if ($src.PSObject.Properties.Name -eq $prop) {
                    $dstProp = $dst."$prop"
                    $srcProp = $src."$prop"
                    $dstPropType = $dstProp.GetType().Name
                    $srcPropType = $srcProp.GetType().Name
                    if($dstPropType -eq 'Int32' -and $srcPropType -eq 'Int64')
                    {
                        $dstPropType = 'Int64'
                    }
                    
                    if ($srcPropType -eq "PSCustomObject" -and $dstPropType -eq "OrderedDictionary") {
                        MergeCustomObjectIntoOrderedDictionary -dst $dst."$prop".Value -src $srcProp
                    }
                    elseif ($dstPropType -ne $srcPropType) {
                        throw "property $prop should be of type $dstPropType, is $srcPropType."
                    }
                    else {
                        if ($srcProp -is [Object[]]) {
                            $srcProp | ForEach-Object {
                                $srcElm = $_
                                $srcElmType = $srcElm.GetType().Name
                                if ($srcElmType -eq "PSCustomObject") {
                                    $ht = [ordered]@{}
                                    $srcElm.PSObject.Properties | Sort-Object -Property Name -Culture "iv-iv" | ForEach-Object { $ht[$_.Name] = $_.Value }
                                    $dst."$prop" += @($ht)
                                }
                                else {
                                    $dst."$prop" += $srcElm
                                }
                            }
                        }
                        else {
                            Write-PSFMessage -Level Verbose -Message "Searching fscps.tools.settings.*.$prop"
                            $setting = Get-PSFConfig -FullName "fscps.tools.settings.*.$prop"
                            Write-PSFMessage -Level Verbose -Message "Found $setting"
                            if($setting)
                            {
                                Set-PSFConfig -FullName $setting.FullName -Value $srcProp
                            }
                            #$dst."$prop" = $srcProp
                        }
                    }
                }
            }
        }
    }
    process{
        Invoke-TimeSignal -Start    
        $res = Get-FSCPSSettings -OutputAsHashtable

        $settingsFiles | ForEach-Object {
            $settingsFile = $_
            if($RepositoryRootPath)
            {
                $settingsPath = Join-Path $RepositoryRootPath $settingsFile
            }
            else {
                $settingsPath = $SettingsFilePath
            }
            
            Write-PSFMessage -Level Verbose -Message "Settings file '$settingsFile' - $(If (Test-Path $settingsPath) {"exists. Processing..."} Else {"not exists. Skip."})"
            if (Test-Path $settingsPath) {
                try {
                    $settingsJson = Get-Content $settingsPath -Encoding UTF8 | ConvertFrom-Json
        
                    # check settingsJson.version and do modifications if needed
                    MergeCustomObjectIntoOrderedDictionary -dst $res -src $settingsJson
                }
                catch {
                    Write-PSFMessage -Level Host -Message "Settings file $settingsPath, is wrongly formatted." -Exception $PSItem.Exception
                    Stop-PSFFunction -Message "Stopping because of errors"
                    return
                    throw 
                }
            }
            Write-PSFMessage -Level Verbose -Message "Settings file '$settingsFile' - processed"
        }
        Write-PSFMessage -Level Host  -Message "Settings were updated succesfully."
        Invoke-TimeSignal -End
    }
    end{

    }

}


<#
    .SYNOPSIS
        Installation of Nuget CLI
         
    .DESCRIPTION
        Download latest Nuget CLI
         
    .PARAMETER MetadataPath
        Path to the local Metadata folder
         
    .PARAMETER Url
        Url/Uri to zip file contains code/package/axmodel
         
    .PARAMETER FileName
        The name of the file should be downloaded by the url. Use if the url doesnt contain the filename.
         
    .EXAMPLE
        PS C:\> Update-FSCPSISVSource MetadataPath "C:\temp\PackagesLocalDirectory" -Url "https://ciellosarchive.blob.core.windows.net/test/Main-Extension-10.0.39_20240516.263.zip?sv=2023-01-03&st=2024-05-21T14%3A26%3A41Z&se=2034-05-22T14%3A26%3A00Z&sr=b&sp=r&sig=W%2FbS1bQrr59i%2FBSHWsftkfNsE1HvFXTrICwZSFiUItg%3D""
         
        This will update the local metadata with the source from the downloaded zip archive.
         
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Update-FSCPSISVSource {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(HelpMessage = "The path to the metadata", Mandatory = $true)]
        [string] $MetadataPath,
        [Parameter(HelpMessage = "The url to the file contains the D365FSC axmodel/modelSourceCode/deployablePackage", Mandatory = $true)]
        [string] $Url,
        [Parameter(HelpMessage = "The name of the downloading file", Mandatory = $false)]
        [string] $FileName
    )
    begin
    {
        try 
        {
            if([string]::IsNullOrEmpty($FileName))
            {
                $_tmpUrlPart = ([uri]$Url).Segments[-1]
                if($_tmpUrlPart.Contains("."))
                {
                    $FileName = $_tmpUrlPart
                }            
            }

            if([string]::IsNullOrEmpty($FileName))
            {
                throw "FileName is empty or cannot be parsed from the url. Please specify the FileName parameter."
            }
            
            if( (-not $FileName.Contains(".zip")) -and (-not $FileName.Contains(".axmodel")) )
            {
                throw "Only a zip or axmodel file can be processed."
            }

            if(Test-Path "$($MetadataPath)/PackagesLocalDirectory")
            {
                $MetadataPath = (Join-Path $($MetadataPath) "/PackagesLocalDirectory")
            }
            elseif(Test-Path "$($MetadataPath)/Metadata")
            {
                $MetadataPath = (Join-Path $($MetadataPath) "/Metadata")
            }

            #$script:DefaultTempPath
            $tempPath = Join-Path -Path $script:DefaultTempPath -ChildPath "updateSource"

            #Cleanup existing temp folder
            Remove-Item -Path $tempPath -Recurse -Force -ErrorAction SilentlyContinue -Confirm:$false        
            $downloadPath = Join-Path -Path $tempPath -ChildPath $fileName
            if (-not (Test-PathExists -Path $tempPath -Type Container -Create)) { return }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
    }
    process
    {
        if (Test-PSFFunctionInterrupt) { return }
        $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve
        . ($helperPath)    
        try {
            
            Write-PSFMessage -Level Important -Message "Downloading $($FileName)" -Target $downloadPath
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
            #[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}

            Write-PSFMessage -Level Important -Message "Source: $Url"
            Write-PSFMessage -Level Important -Message "Destination $downloadPath"

            Start-BitsTransfer -Source $Url -Destination $downloadPath


            #check is archive contains few archives
            $packagesPaths = [System.Collections.ArrayList]@()
            $sourceCodePaths = [System.Collections.ArrayList]@()
            $axmodelsPaths = [System.Collections.ArrayList]@()

            if($downloadPath.EndsWith(".zip"))
            {            
                Unblock-File $downloadPath
                Expand-7zipArchive -Path $downloadPath -DestinationPath "$tempPath/archives"

                $ispackage = Get-ChildItem -Path "$tempPath/archives" -Filter 'AXUpdateInstaller.exe' -ErrorAction SilentlyContinue -Force    

                if($ispackage)
                {
                    $null = $packagesPaths.Add($downloadPath)
                }
                else
                {   
                    Get-ChildItem "$tempPath/archives" -Filter '*.zip' -Recurse  -ErrorAction SilentlyContinue -Force | ForEach-Object{
                        $archive = $_.FullName
                        
                        $tmpArchivePath = Join-Path "$tempPath/archives" $_.BaseName
                        
                        Unblock-File $archive
                        Expand-7zipArchive -Path $archive -DestinationPath $tmpArchivePath
                        $ispackage = Get-ChildItem -Path $tmpArchivePath -Filter 'AXUpdateInstaller.exe' -Recurse -ErrorAction SilentlyContinue -Force    
    
                        if($ispackage)
                        {
                            $null = $packagesPaths.Add($_.FullName)
                        }
                        else
                        {   
                            if($_.FullName -notlike "*dynamicsax-*.zip")
                            {
                                $null = $sourceCodePaths.Add($_.FullName)
                            }
                        }
                            
                    } 
                    #check axmodel files inside and add to list if found
                    Get-ChildItem "$tempPath/archives" -Filter '*.axmodel' -Recurse -ErrorAction SilentlyContinue -Force | ForEach-Object {
                        $null = $axmodelsPaths.Add($_.FullName)
                    }           
                } 
            }
            if($downloadPath.EndsWith(".axmodel"))
            {
                $null = $axmodelsPaths.Add($_.FullName)
            }

            foreach($package in $packagesPaths)
            {
                try {
                    $package = Get-ChildItem $package
                    Write-PSFMessage -Level Important -Message "The package $($package.BaseName) importing..."
                    $tmpPackagePath = Join-Path "$tempPath/packages" $package.BaseName
                    Unblock-File $package
                    Expand-7zipArchive -Path $package -DestinationPath $tmpPackagePath
                    $models = Get-ChildItem -Path $tmpPackagePath -Filter "dynamicsax-*.zip" -Recurse -ErrorAction SilentlyContinue -Force
                    foreach($model in $models)
                    {            
                        Write-PSFMessage -Level Important -Message "$($model.BaseName) processing..."
                        $zipFile = [IO.Compression.ZipFile]::OpenRead($model.FullName)
                        $zipFile.Entries | Where-Object {$_.FullName.Contains(".xref")} | ForEach-Object{
                            $modelName = $_.Name.Replace(".xref", "")
                            $targetModelPath = (Join-Path $MetadataPath "$modelName/")   
                            if(Test-Path $targetModelPath)
                            {
                                Remove-Item $targetModelPath -Recurse -Force
                            }      
                            Write-PSFMessage -Level Important -Message "'$($model.FullName)' to the $($targetModelPath)..."
                            Expand-7zipArchive -Path $model.FullName -DestinationPath $targetModelPath
                        }            
                        $zipFile.Dispose()
                    }
                    Write-PSFMessage -Level Important -Message "The package $($package) imported"
                }
                catch {
                    Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception
                    Write-PSFMessage -Level Important -Message "The package $($package) is not imported"
                }               
            }

            if(($axmodelsPaths.Count -gt 0) -and ($PSVersionTable.PSVersion.Major -gt 5)) {
                Write-PSFMessage -Level Warning -Message "The axmodel cannot be imported. Current PS version is $($PSVersionTable.PSVersion). The latest PS major version acceptable to import the axmodel is 5."
            }
            else {

                $PlatformVersion = (Get-FSCPSVersionInfo -Version 10.0.38).data.PlatformVersion
                $nugetsPath = Join-Path $tempPath "NuGets"
                $compilerNugetPath = Join-Path $nugetsPath "Microsoft.Dynamics.AX.Platform.CompilerPackage.$PlatformVersion.nupkg"
                $compilerPath = Join-Path $tempPath "Microsoft.Dynamics.AX.Platform.CompilerPackage.$PlatformVersion"

                $null = Test-PathExists -Path $compilerPath -Type Container -Create
                $null = Test-PathExists -Path $nugetsPath -Type Container -Create
                Write-PSFMessage -Level Important -Message "The $PlatformVersion Platform Version used."
                Get-FSCPSNuget -Version $PlatformVersion -Type PlatformCompilerPackage -Path $nugetsPath
                Write-PSFMessage -Level Important -Message "The PlatformCompiler NuGet were downloaded at $nugetsPath."
                Expand-7zipArchive -Path $compilerNugetPath -DestinationPath $compilerPath
                $curLocation = Get-Location
                Set-Location $compilerPath
                                
                try {
                    $miscPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\misc"
                    Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.Client.dll" -Destination $compilerPath -Force
                    Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.Common.dll" -Destination $compilerPath -Force               
                    Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.Diff.dll" -Destination $compilerPath -Force
                    Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.VersionControl.Client.dll" -Destination $compilerPath -Force
                    Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.VersionControl.Common.dll" -Destination $compilerPath -Force
                }
                catch {
                    Write-PSFMessage -Level Important -Message $_.Exception.Message
                }

                foreach($axModel in $axmodelsPaths)
                {
                    try {
                        Write-PSFMessage -Level Important -Message "The axmodel $($axModel) importing..."
                        Enable-D365Exception
                        
                        #Import-D365Model -Path $axModel -MetaDataDir $MetadataPath -BinDir $compilerPath -Replace

                        Invoke-ModelUtil -Path $axModel -MetaDataDir $MetadataPath -BinDir $compilerPath -Command Replace


                        Disable-D365Exception
                        Write-PSFMessage -Level Important -Message "The axmodel $($axModel) imported."
                    }
                    catch {
                        Disable-D365Exception
                        Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception
                        Write-PSFMessage -Level Important -Message "The axmodel $($axModel) is not imported."
                    }                   
                }
                Set-Location $curLocation
            }            

            foreach($sourceCode in $sourceCodePaths)
            {
                try {
                    Write-PSFMessage -Level Important -Message "The source code $($sourceCode) importing..."
                    $zipFile = [IO.Compression.ZipFile]::OpenRead($sourceCode)
                        $zipFile.Entries | Where-Object {$_.FullName.Contains(".xref")} | ForEach-Object{
                            $modelName = $_.Name.Replace(".xref", "")
                            $targetModelPath = (Join-Path $MetadataPath "$modelName/")   
                            Remove-Item $targetModelPath -Recurse -Force 
                            Expand-7zipArchive -Path $($sourceCode) -DestinationPath $targetModelPath
                        }            
                        $zipFile.Dispose()
                    Write-PSFMessage -Level Important -Message "The source code $($sourceCode) imported"
                }
                catch {
                    Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception
                    Write-PSFMessage -Level Important -Message "The source code $($sourceCode) is not imported"
                }
            }

            ## Cleanup XppMetadata
            Get-ChildItem -Path $MetadataPath -Directory -Filter "*XppMetadata" -Recurse | ForEach-Object { Remove-Item -Path $_.FullName -Recurse -Force }
        }
        catch {
            Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
            return
        }
    }
    end{

    } 
}


<#
    .SYNOPSIS
        This updates the D365FSC model version
         
    .DESCRIPTION
        This updates the D365FSC model version
         
    .PARAMETER xppSourcePath
        Path to the xpp metadata folder
         
    .PARAMETER xppDescriptorSearch
        Descriptor search pattern
         
    .PARAMETER xppLayer
        Layer of the code
         
    .PARAMETER versionNumber
        Target model version change to
         
    .EXAMPLE
        PS C:\> Update-FSCPSModelVersion -xppSourcePath "c:\temp\metadata" -xppLayer "ISV" -versionNumber "5.4.8.4" -xppDescriptorSearch $("TestModel"+"\Descriptor\*.xml")
         
        this will change the version of the TestModel to 5.4.8.4
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Update-FSCPSModelVersion {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$xppSourcePath,
        [Parameter()]
        [string]$xppDescriptorSearch,
        $xppLayer,
        $versionNumber
    )

    begin{
        Invoke-TimeSignal -Start
        Write-PSFMessage -Level Important -Message "xppSourcePath: $xppSourcePath"
        Write-PSFMessage -Level Important -Message "xppDescriptorSearch: $xppDescriptorSearch"
        Write-PSFMessage -Level Important -Message "xppLayer: $xppLayer"
        Write-PSFMessage -Level Important -Message "versionNumber: $versionNumber"

        if ($xppDescriptorSearch.Contains("`n"))
        {
            [string[]]$xppDescriptorSearch = $xppDescriptorSearch -split "`n"
        }
        
        $null = Test-Path -LiteralPath $xppSourcePath -PathType Container
            
        if ($versionNumber -match "^\d+\.\d+\.\d+\.\d+$")
        {
            $versions = $versionNumber.Split('.')
        }
        else
        {
            throw "Version Number '$versionNumber' is not of format #.#.#.#"
        }
        switch ( $xppLayer )
        {
            "SYS" { $xppLayer = 0 }
            "SYP" { $xppLayer = 1 }
            "GLS" { $xppLayer = 2 }
            "GLP" { $xppLayer = 3 }
            "FPK" { $xppLayer = 4 }
            "FPP" { $xppLayer = 5 }
            "SLN" { $xppLayer = 6 }
            "SLP" { $xppLayer = 7 }
            "ISV" { $xppLayer = 8 }
            "ISP" { $xppLayer = 9 }
            "VAR" { $xppLayer = 10 }
            "VAP" { $xppLayer = 11 }
            "CUS" { $xppLayer = 12 }
            "CUP" { $xppLayer = 13 }
            "USR" { $xppLayer = 14 }
            "USP" { $xppLayer = 15 }
        }
        
    }
    process{
# Discover packages
    #$BuildModuleDirectories = @(Get-ChildItem -Path $BuildMetadataDir -Directory)
    #foreach ($BuildModuleDirectory in $BuildModuleDirectories)
    #{
        $potentialDescriptors = Find-FSCPSMatch -DefaultRoot $xppSourcePath -Pattern $xppDescriptorSearch | Where-Object { (Test-Path -LiteralPath $_ -PathType Leaf) }
        if ($potentialDescriptors.Length -gt 0)
        {
            Write-PSFMessage -Level Verbose -Message "Found $($potentialDescriptors.Length) potential descriptors"
    
            foreach ($descriptorFile in $potentialDescriptors)
            {
                try
                {
                    [xml]$xml = Get-Content $descriptorFile -Encoding UTF8
    
                    $modelInfo = $xml.SelectNodes("/AxModelInfo")
                    if ($modelInfo.Count -eq 1)
                    {
                        $layer = $xml.SelectNodes("/AxModelInfo/Layer")[0]
                        $layerid = $layer.InnerText
                        $layerid = [int]$layerid
    
                        $modelName = ($xml.SelectNodes("/AxModelInfo/Name")).InnerText
                            
                        # If this model's layer is equal or above lowest layer specified
                        if ($layerid -ge $xppLayer)
                        {
                            $version = $xml.SelectNodes("/AxModelInfo/VersionMajor")[0]
                            $version.InnerText = $versions[0]
    
                            $version = $xml.SelectNodes("/AxModelInfo/VersionMinor")[0]
                            $version.InnerText = $versions[1]
    
                            $version = $xml.SelectNodes("/AxModelInfo/VersionBuild")[0]
                            $version.InnerText = $versions[2]
    
                            $version = $xml.SelectNodes("/AxModelInfo/VersionRevision")[0]
                            $version.InnerText = $versions[3]
    
                            $xml.Save($descriptorFile)
    
                            Write-PSFMessage -Level Verbose -Message " - Updated model $modelName version to $versionNumber in $descriptorFile"
                        }
                        else
                        {
                            Write-PSFMessage -Level Verbose -Message " - Skipped $modelName because it is in a lower layer in $descriptorFile"
                        }
                    }
                    else
                    {
                        Write-PSFMessage -Level Error -Message "File '$descriptorFile' is not a valid descriptor file"
                    }
                }
                catch {
                    Write-PSFMessage -Level Host -Message "Something went wrong while updating D365FSC package versiob" -Exception $PSItem.Exception
                    Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true
                    return
                }
                finally{

                }
            }
        }
    #}
    }
    end{
        Invoke-TimeSignal -End
    }          
}