commands.ps1


<#
    .SYNOPSIS
        Clear the imported AX 2012 PowerShell modules
         
    .DESCRIPTION
        Removes the different AX 2012 PowerShell modules that has been loaded into the session.
         
    .EXAMPLE
        PS C:\> Clear-Ax2012StandardPowershellModule
         
        This will remove all the known AX 2012 PowerShell modules that have been loaded.
        It is connected to the use of Import-Module $Script:AxPowerShellModule
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Clear-Ax2012StandardPowershellModule {
    [CmdletBinding()]
    param ( )

    Remove-Module @("AxUtilLib", "AxUtilLib.PowerShell", "Microsoft.Dynamics.Administration", "Microsoft.Dynamics.AX.Framework.Management", "Microsoft.Dynamics.ManagementUtilities")
}


<#
    .SYNOPSIS
        Convert HashTable into an array
         
    .DESCRIPTION
        Convert HashTable into an array of Key and Value
         
    .PARAMETER InputObject
        The HashTable object that you want to work against
         
    .PARAMETER KeyPrefix
        The prefix that you want to append to the key of the HashTable
         
        The default value is "-"
         
    .PARAMETER ValuePrefix
        The prefix that you want to append to the value of the HashTable
         
        The default value is " "
         
    .EXAMPLE
        PS C:\> $params = @{DatabaseServer = "Localhost"; DatabaseName = "MicrosoftDynamicsAx_model"}
        PS C:\> $arguments = Convert-HashToArgString -Inputs $params
         
        This will convert the $params into an array of strings, each with the Key and Value.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Convert-HashToArgString {
    [CmdletBinding()]
    param (
        [HashTable] $InputObject,

        [string] $KeyPrefix = "-",

        [string] $ValuePrefix = " "
    )

    $InputObject.Keys | ForEach-Object { "$KeyPrefix$($_)$ValuePrefix`"$($InputObject.Item($_))`""}
}


<#
    .SYNOPSIS
        Convert HashTable into an array
         
    .DESCRIPTION
        Convert HashTable with switches inside into an array of Key:Value
         
    .PARAMETER InputObject
        The HashTable object that you want to work against
         
        Shold only contain Key / Vaule, where value is $true or $false
         
    .PARAMETER KeyPrefix
        The prefix that you want to append to the key of the HashTable
         
        The default value is "-"
         
    .PARAMETER ValuePrefix
        The prefix that you want to append to the value of the HashTable
         
        The default value is ":"
         
    .EXAMPLE
        PS C:\> $params = @{NoPrompt = $true; CreateParents = $false}
        PS C:\> $arguments = Convert-HashToArgStringSwitch -Inputs $params
         
        This will convert the $params into an array of strings, each with the Key:Value.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Convert-HashToArgStringSwitch {
    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [HashTable] $InputObject,

        [string] $KeyPrefix = "-",

        [string] $ValuePrefix = ":"
    )

    foreach ($key in $InputObject.Keys) {
        $value = "`${0}" -f $InputObject.Item($key).ToString().ToLower()
        "$KeyPrefix$($key)$ValuePrefix$($value)"
    }
}


<#
    .SYNOPSIS
        Get the autorun xml element
         
    .DESCRIPTION
        Get the correct autorun xml element based on the action that you want to perform
         
    .PARAMETER Action
        The action name that you want to execute when running the autorun feature of the AX 2012 Client
         
        Valid options are:
        "CompileCilFull"
        "CompileCilIncremental"
        "CompileXpp"
        "CompileXppAndXRef"
        "SynchronizeDB"
         
    .EXAMPLE
        PS C:\> Get-AutoRunXML -Action "SynchronizeDB"
         
        This will return the correct XML element for "SynchronizeDB".
         
    .NOTES
        Tags:
         
        Author: M�tz Jensen (@Splaxi)
         
        #https://docs.microsoft.com/en-us/previous-versions/dynamics/ax-2012/application-classes/gg922801%28v%3dax.60%29
#>


function Get-AutoRunXML {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [String] $Action
    )
     
    $res = ""

    switch ($Action) {
        "SynchronizeDB" {
            $res = '<Synchronize syncDb="true"/>'
            # $element = $xml.CreateElement('Synchronize')
            # $element.SetAttribute("syncDb", "true")
        }

        "SynchronizeRoles" {
            # $res = '<Synchronize "syncDb"="true"/>'
            # $element = $xml.CreateElement('Synchronize')
            # $element.SetAttribute("syncRoles", "true")
        }

        "CompileCilFull" {
            $res = '<CompileIL incremental="false"/>'
            # $element = $xml.CreateElement('CompileIL')
            # $element.SetAttribute("incremental", "false")
        }

        "CompileCilIncremental" {
            $res = '<CompileIL incremental="true"/>'
            # $element = $xml.CreateElement('CompileIL')
            # $element.SetAttribute("incremental", "true")
        }

        "CompileXpp" {
            $res = '<CompileApplication crossReference="false"/>'
            # $element = $xml.CreateElement('CompileApplication')
            # $element.SetAttribute("crossReference", "false")
        }

        "CompileXppAndXRef" {
            $res = '<CompileApplication crossReference="true"/>'
            # $element = $xml.CreateElement('CompileApplication')
            # $element.SetAttribute("crossReference", "true")
        }
    }

    $res
}


<#
    .SYNOPSIS
        Get details about an AX 2012 AOS instance
         
    .DESCRIPTION
        Get all the technical details about an AX 2012 AOS instance
         
    .PARAMETER RegistryPath
        Path to the registry for the specific AX 2012 AOS instance
         
    .EXAMPLE
        PS C:\> Get-AxAosInstanceDetails -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Dynamics Server\6.0\01"
         
        This will traverse all the details about the first installed AX 2012 AOS instance in the registry.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxAosInstanceDetails {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    param(
        [string] $RegistryPath
    )

    Write-PSFMessage -Level Verbose -Message "Working against $RegistryPath" -Target $RegistryPath

    $RegKey = Get-Item -Path $RegistryPath.Replace("HKEY_LOCAL_MACHINE", "HKLM:")
    $RegOuter = Get-ItemProperty -Path ($RegKey.Name).Replace("HKEY_LOCAL_MACHINE", "HKLM:")
    $RegInner = Get-ItemProperty -Path (Join-Path $RegKey.Name $RegOuter.Current).Replace("HKEY_LOCAL_MACHINE", "HKLM:")
    $BuildNumbers = Get-FileVersion -Path $(Join-Path $RegInner.bindir "Ax32Serv.exe")

    $InstanceDetail = [Ordered]@{}
    
    $InstanceDetail.InstanceName = $RegOuter.InstanceName
    $InstanceDetail.ConfigurationName = $RegOuter.Current
    $InstanceDetail.BinDirectory = $RegInner.bindir
    $InstanceDetail.ExecutablePath = Join-Path $RegInner.bindir "Ax32Serv.exe"

    $InstanceDetail.FileVersion = $BuildNumbers.FileVersion
    $InstanceDetail.ProductVersion = $BuildNumbers.ProductVersion
    $InstanceDetail.FileVersionUpdated = $BuildNumbers.FileVersionUpdated
    $InstanceDetail.ProductVersionUpdated = $BuildNumbers.ProductVersionUpdated

    $InstanceDetail.DatabaseServer = $RegInner.dbserver
    $InstanceDetail.DatabaseName = $RegInner.database
    $InstanceDetail.ModelstoreDatabase = "$($RegInner.database)_model"

    $InstanceDetail.AosPort = $RegInner.port
    $InstanceDetail.WsdlPort = $RegInner.WSDLPort
    $InstanceDetail.NetTcpPort = $RegInner.NetTCPPort
    
    $InstanceDetail.RegistryKeyPath = $RegKey.Name
    $InstanceDetail.InstanceNumber = Split-Path -Path $RegKey.Name -Leaf
    $InstanceDetail.ComputerName = "$env:computername"

    [PSCustomObject] $InstanceDetail
}


<#
    .SYNOPSIS
        Get the AX 2012 Client bin directory
         
    .DESCRIPTION
        Get the AX 2012 Client bin directory from the registry
         
    .EXAMPLE
        PS C:\> Get-ClientBinDir
         
        This will get the full path for the AX 2012 client bin directory
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-ClientBinDir {
    [CmdletBinding()]
    [OutputType('System.String')]
    param ( )

    $RegKey = Get-Item -Path $Script:RegistryClient -ErrorAction SilentlyContinue
    
    if (-not ($null -eq $RegKey)) {
        Join-Path $RegKey.GetValue("InstallDir32") "Client\Bin"
    }
}


<#
    .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
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-DeepClone {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
    [CmdletBinding()]
    param(
        [parameter(Mandatory = $true)]
        $InputObject
    )
    process {
        if ($InputObject -is [hashtable]) {
            $clone = @{}
            foreach ($key in $InputObject.keys) {
                $clone[$key] = Get-DeepClone $InputObject[$key]
            }
            $clone
        }
        else {
            $InputObject
        }
    }
}


<#
    .SYNOPSIS
        Get details about the user
         
    .DESCRIPTION
        Extract the FQDM and Sid from the user account
         
    .PARAMETER Username
        User name with the domain name included, in either PRE-2000 or UPN style
         
    .EXAMPLE
        PS C:\> Get-DomainUserDetails -Username "Test@ACME.LOCAL"
         
        This will return the details from the user account "Test@ACME.LOCAL".
         
    .EXAMPLE
        PS C:\> Get-DomainUserDetails -Username "ACME.LOCAL\Test"
         
        This will return the details from the user account "ACME.LOCAL\Test".
         
    .EXAMPLE
        PS C:\> Get-DomainUserDetails -Username "ACME\Test"
         
        This will return the details from the user account "ACME\Test".
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
#>


function Get-DomainUserDetails {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    param (
        [string] $Username
    )

    $res = [Ordered]@{}

    $domain = ""

    if ($Username -like "*@*") {
        $domain = $Username.Split("@")[1]
        $res.UserId = $Username.Split("@")[0]
    }
    else {
        $domain = $Username.Split("\")[0]
        $res.UserId = $Username.Split("\")[1]
    }

    $domains = Get-CimInstance Win32_NTDomain

    foreach ($item in $domains) {

        if ($item.DnsForestName -like "$($item.DomainName)*") {
            if (($item.DnsForestName -like "$domain" -or $item.DomainName -like "$domain")) {
                $res.Domain = $item.DnsForestName
                break
            }
        }
        elseIf ("$($item.DomainName).$($item.DnsForestName)".ToLower() -eq $domain.ToLower() -or $item.DomainName -like "$domain") {
            $res.Domain = "$($item.DomainName).$($item.DnsForestName)".ToLower()
            break
        }
    }

    $res.Sid = Get-Sid -Username $Username

    [PSCustomObject]$res
}


<#
    .SYNOPSIS
        Get the file version details
         
    .DESCRIPTION
        Get the file version details for any given file
         
    .PARAMETER Path
        Path to the file that you want to extract the file version details from
         
    .EXAMPLE
        PS C:\> Get-FileVersion -Path "C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\Bin\AxServ32.exe"
         
        This will get the file version details for the AX AOS executable (AxServ32.exe).
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
        Inspired by https://blogs.technet.microsoft.com/askpfeplat/2014/12/07/how-to-correctly-check-file-versions-with-powershell/
         
#>

function Get-FileVersion {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [string] $Path
    )

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

    Write-PSFMessage -Level Verbose -Message "Extracting the file properties for: $Path" -Target $Path
    
    $Filepath = Get-Item -Path $Path

    [PSCustomObject]@{
        FileVersion           = $Filepath.VersionInfo.FileVersion
        ProductVersion        = $Filepath.VersionInfo.ProductVersion
        FileVersionUpdated    = "$($Filepath.VersionInfo.FileMajorPart).$($Filepath.VersionInfo.FileMinorPart).$($Filepath.VersionInfo.FileBuildPart).$($Filepath.VersionInfo.FilePrivatePart)"
        ProductVersionUpdated = "$($Filepath.VersionInfo.ProductMajorPart).$($Filepath.VersionInfo.ProductMinorPart).$($Filepath.VersionInfo.ProductBuildPart).$($Filepath.VersionInfo.ProductPrivatePart)"
    }
}


<#
    .SYNOPSIS
        Get key from HashTable
         
    .DESCRIPTION
        Get specific key(s) from a HashTable returned as a HashTable
         
    .PARAMETER InputObject
        The HashTable that you want to extract key(s) from
         
    .PARAMETER Keys
        Names of the key(s) that you want to to extract from the HashTable
         
    .EXAMPLE
        PS C:\> $params = @{NoPrompt = $true; CreateParent = $false}
         
        Get-HashtableKey -InputObject $params -Keys "NoPrompt"
         
        This will return a new HashTable only containing the "NoPrompt" entry.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-HashtableKey {
    [CmdletBinding()]
    [OutputType([HashTable])]
    param (
        [parameter(Mandatory = $true)]
        [HashTable] $InputObject,

        [Parameter(Mandatory = $true)]
        [string[]] $Keys
        
    )

    $var = Get-DeepClone $InputObject
    $res = @{}

    foreach ($key in $var.Keys) {
        Write-PSFMessage -Level Verbose -Message "Working on key: $key" -Target $key
        
        if($Keys.Contains($key)) {
            Write-PSFMessage -Level Verbose -Message "Found key: $key" -Target $key
            $null = $res.Add($key, $var.Item($key))
        }
    }

    $res
}


<#
    .SYNOPSIS
        Get service list from HashTable
         
    .DESCRIPTION
        Extract the services from the list of entries in the HashTable
         
    .PARAMETER AllAxServices
        Instruct the cmdlet to return all services
         
    .PARAMETER Aos
        Instruct the cmdlet to return AOS
         
    .PARAMETER ManagementReporter
        Instruct the cmdlet to return ManagementReporter
         
    .PARAMETER DIXF
        Instruct the cmdlet to return DIXF
         
    .EXAMPLE
        PS C:\> Get-ServiceList -All
         
        This will return all services that the cmdlet knows about.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Get-ServiceList {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [switch] $AllAxServices,

        [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 2 )]
        [switch] $Aos,

        [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 3 )]
        [switch] $ManagementReporter,

        [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 4 )]
        [switch] $DIXF
    )

    if ($PSCmdlet.ParameterSetName -eq "Specific") {
        $AllAxServices = $false
    }

    Write-PSFMessage -Level Verbose -Message "The PSBoundParameters was" -Target ($PSBoundParameters.Keys -Join "," )
    
    $mrProcessName = "MR2012ProcessService"
    $mrApplicationName = "MR2012ApplicationService"

    $dixfName = "Microsoft.Dynamics.AX.Framework.Tools.DMF.SSISHelperService.exe"

    [System.Collections.ArrayList]$Services = New-Object -TypeName "System.Collections.ArrayList"

    if ($AllAxServices) {
        for ($i = 1; $i -lt 100; $i++) {
            $null = $Services.Add("AOS60`$$($i.ToString("00"))")
        }

        $null = $Services.AddRange(@($mrProcessName, $mrApplicationName, $dixfName))
    }
    else {
        if ($Aos) {
            for ($i = 1; $i -lt 100; $i++) {
                $null = $Services.Add("AOS60`$$($i.ToString("00"))")
            }
        }

        if ($ManagementReporter) {
            $null = $Services.Add($mrProcessName)
            $null = $Services.Add($mrApplicationName)
        }

        if ($DIXF) {
            $null = $Services.Add($dixfName)
        }
    }

    $Services.ToArray()
}


<#
    .SYNOPSIS
        Get the Sid from the user
         
    .DESCRIPTION
        Translate the username into the unique sid of the user
         
    .PARAMETER Username
        User name with the domain name included, in either PRE-2000 or UPN style
         
    .EXAMPLE
        PS C:\> Get-Sid -Username "Test@ACME.LOCAL"
         
        This will return the Sid from the user account "Test@ACME.LOCAL".
         
    .NOTES
        Tags:
         
        Author: M�tz Jensen (@Splaxi)
         
#>


function Get-Sid {
    [CmdletBinding()]
    param (
        [string] $Username
    )

    (New-Object System.Security.Principal.NTAccount($Username)).Translate([System.Security.Principal.SecurityIdentifier]).value
}


<#
    .SYNOPSIS
        Get a SqlCommand object
         
    .DESCRIPTION
        Get a SqlCommand object initialized with the passed parameters
         
    .PARAMETER DatabaseServer
        The name of the database server
         
    .PARAMETER DatabaseName
        The name of the database
         
    .PARAMETER SqlUser
        The login name for the SQL Server instance
         
    .PARAMETER SqlPwd
        The password for the SQL Server user.
         
    .PARAMETER TrustedConnection
        Should the connection use a Trusted Connection or not
         
    .EXAMPLE
        PS C:\> Get-SqlCommand -DatabaseServer localhost -DatabaseName MicrosoftDynamicsAx_model -SqlUser User123 -SqlPwd "Password123" -TrustedConnection $false
         
        This will initialize a new SqlCommand object (.NET type) with localhost as the server name, AxDB as the database and the User123 sql credentials.
         
    .EXAMPLE
        PS C:\> Get-SqlCommand -DatabaseServer localhost -DatabaseName MicrosoftDynamicsAx_model -TrustedConnection $true
         
        This will initialize a new SqlCommand object (.NET type) with localhost as the server name, AxDB as the database and use the current windows credentials (trusted connection).
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-SQLCommand {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DatabaseServer,

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

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

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

        [Parameter(Mandatory = $false)]
        [boolean] $TrustedConnection
    )

    Write-PSFMessage -Level Debug -Message "Writing the bound parameters" -Target $PsBoundParameters
    [System.Collections.ArrayList]$Params = New-Object -TypeName "System.Collections.ArrayList"

    $null = $Params.Add("Server='$DatabaseServer';")
    $null = $Params.Add("Database='$DatabaseName';")

    if ($null -eq $TrustedConnection -or (-not $TrustedConnection)) {
        $null = $Params.Add("User='$SqlUser';")
        $null = $Params.Add("Password='$SqlPwd';")
    }
    else {
        $null = $Params.Add("Integrated Security='SSPI';")
    }

    $null = $Params.Add("Application Name='ax2012.tools'")
    
    Write-PSFMessage -Level Verbose -Message "Building the SQL connection string." -Target ($Params.ToArray() -join ",")
    $sqlConnection = New-Object System.Data.SqlClient.SqlConnection

    try {
        $sqlConnection.ConnectionString = ($Params -join "")

        $sqlCommand = New-Object System.Data.SqlClient.SqlCommand
        $sqlCommand.Connection = $sqlConnection
        $sqlCommand.CommandTimeout = 0
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while working with the sql server connection objects" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors"
        return
    }
    
    $sqlCommand
}


<#
    .SYNOPSIS
        Get the size from the parameter
         
    .DESCRIPTION
        Get the size from the parameter based on its datatype and value
         
    .PARAMETER SqlParameter
        The SqlParameter object that you want to get the size from
         
    .EXAMPLE
        PS C:\> $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
        PS C:\> $SqlCmd.Parameters.AddWithValue("@Parm1", "1234")
        PS C:\> Get-SqlParameterSize -SqlParameter $SqlCmd.Parameters[0]
         
        This will extract the size from the first parameter from the SqlCommand object and return it as a formatted string.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-SqlParameterSize {
    [CmdletBinding()]
    [OutputType('System.String')]
    param (
        [System.Data.SqlClient.SqlParameter] $SqlParameter
    )

    $res = ""

    $stringSizeTypes = @(
        [System.Data.SqlDbType]::Char,
        [System.Data.SqlDbType]::NChar,
        [System.Data.SqlDbType]::NText,
        [System.Data.SqlDbType]::NVarChar,
        [System.Data.SqlDbType]::Text,
        [System.Data.SqlDbType]::VarChar
    )

    if ( $stringSizeTypes -contains $SqlParameter.SqlDbType) {
        $res = "($($SqlParameter.Size))"
    }

    $res
}


<#
    .SYNOPSIS
        Get the value from the parameter
         
    .DESCRIPTION
        Get the value that is assigned to the SqlParameter object
         
    .PARAMETER SqlParameter
        The SqlParameter object that you want to work against
         
    .EXAMPLE
        PS C:\> $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
        PS C:\> $SqlCmd.Parameters.AddWithValue("@Parm1", "1234")
        PS C:\> Get-SqlParameterValue -SqlParameter $SqlCmd.Parameters[0]
         
        This will extract the value from the first parameter from the SqlCommand object.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-SqlParameterValue {
    [CmdletBinding()]
    [OutputType('System.String')]
    param (
        [System.Data.SqlClient.SqlParameter] $SqlParameter
    )

    $result = $null

    $stringEscaped = @(
        [System.Data.SqlDbType]::Char,
        [System.Data.SqlDbType]::DateTime,
        [System.Data.SqlDbType]::NChar,
        [System.Data.SqlDbType]::NText,
        [System.Data.SqlDbType]::NVarChar,
        [System.Data.SqlDbType]::Text,
        [System.Data.SqlDbType]::VarChar,
        [System.Data.SqlDbType]::Xml,
        [System.Data.SqlDbType]::Date,
        [System.Data.SqlDbType]::Time,
        [System.Data.SqlDbType]::DateTime2,
        [System.Data.SqlDbType]::DateTimeOffset
    )
    
    $stringNumbers = @([System.Data.SqlDbType]::Float, [System.Data.SqlDbType]::Decimal)
    
    switch ($SqlParameter.SqlDbType) {
        { $stringEscaped -contains $_ } {
            $result = "'{0}'" -f $SqlParameter.Value.ToString().Replace("'", "''")
            break
        }

        { [System.Data.SqlDbType]::Bit } {
            if ((ConvertTo-BooleanOrDefault -Object $SqlParameter.Value.ToString() -Default $true)) {
                $result = '1'
            }
            else {
                $result = '0'
            }
                        
            break
        }
        
        { $stringNumbers -contains $_ } {
            $SqlParameter.Value
            $result = ([System.Double]$SqlParameter.Value).ToString([System.Globalization.CultureInfo]::InvariantCulture).Replace("'", "''")
            break
        }

        default {
            $result = $SqlParameter.Value.ToString().Replace("'", "''")
            break
        }
    }

    $result
}


<#
    .SYNOPSIS
        Get an executable string from a SqlCommand object
         
    .DESCRIPTION
        Get an formatted and valid string from a SqlCommand object that contains all variables
         
    .PARAMETER SqlCommand
        The SqlCommand object that you want to retrieve the string from
         
    .EXAMPLE
        PS C:\> $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
        PS C:\> $SqlCmd.CommandText = "SELECT * FROM Table WHERE Column = @Parm1"
        PS C:\> $SqlCmd.Parameters.AddWithValue("@Parm1", "1234")
        PS C:\> Get-SqlString -SqlCommand $SqlCmd
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-SqlString {
    [CmdletBinding()]
    [OutputType('System.String')]
    param (
        [System.Data.SqlClient.SqlCommand] $SqlCommand
    )

    $sbDeclare = [System.Text.StringBuilder]::new()
    $sbAssignment = [System.Text.StringBuilder]::new()
    $sbRes = [System.Text.StringBuilder]::new()

    if ($SqlCommand.CommandType -eq [System.Data.CommandType]::Text) {
        if (-not ($null -eq $SqlCommand.Connection)) {
            $null = $sbDeclare.Append("USE ").AppendLine($SqlCommand.Connection.Database)
        }

        foreach ($parameter in $SqlCommand.Parameters) {
            if ($parameter.Direction -eq [System.Data.ParameterDirection]::Input) {
                $null = $sbDeclare.Append("DECLARE ").Append($parameter.ParameterName).Append("`t")
                $null = $sbDeclare.Append($parameter.SqlDbType.ToString().ToUpper())
                $null = $sbDeclare.AppendLine((Get-SqlParameterSize -SqlParameter $parameter))

                $null = $sbAssignment.Append("SET ").Append($parameter.ParameterName).Append(" = ").AppendLine((Get-SqlParameterValue -SqlParameter $parameter))
            }
        }
        
        $null = $sbRes.AppendLine($sbDeclare.ToString())
        $null = $sbRes.AppendLine($sbAssignment.ToString())
        $null = $sbRes.AppendLine($SqlCommand.CommandText)
    }

    $sbRes.ToString()
}


<#
    .SYNOPSIS
        Get WMDP details from the IIS
         
    .DESCRIPTION
        Get all the necessary details from the IIS about the WMDP installation
         
    .EXAMPLE
        PS C:\> Get-WMDPDetailsFromIIS
         
        This will get details from all the WMDP installations on the server.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-WMDPDetailsFromIIS {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    param ( )

    if ((Get-Module -ListAvailable -Name "IISAdministration").Count -lt 1) {
        Write-PSFMessage -Level Host -Message "It seems that you didn't have the <c='em'>extended</c> powershell administration tools for IIS installed. For Windows Server 2012 and 2012 R2 it is required that you install this module directly."
        Write-PSFMessage -Level Host -Message "Install-Module IISAdministration -Force -Confirm:`$false"
        Stop-PSFFunction -Message "Stopping because of missing parameters" -StepsUpward 1
        return
    } else {
        $null = Import-Module -name "IISAdministration" -Force
    }

    $sites = Get-IISSite | Where-Object {$_.Applications.VirtualDirectories.PhysicalPath -like "*AX*warehouse*portal*"}

    foreach ($site in $sites) {
        $res = [Ordered]@{SiteId = $site.Id; SiteName = $site.Name;
                SiteStatus = $site.State; SiteBindings = $site.Bindings;
                Path = $site.Applications.VirtualDirectories.PhysicalPath
                MvcViewPath = Join-Path $site.Applications.VirtualDirectories.PhysicalPath "Views\Execute"
                CssPath = Join-Path $site.Applications.VirtualDirectories.PhysicalPath "Content\CSS\RFCSS"
            }
        
        $appPool = Get-IISAppPool | Where-Object Name -eq $site.Applications.ApplicationPoolName
        $res.AppPoolName = $appPool.Name
        $res.AppPoolStatus = $appPool.Status
        $res.AppPoolIdentity = $appPool.ProcessModel.Username

        [PSCustomObject]$res
    }
}


<#
    .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 TimeoutInMinutes
        Number of minutes the process is allowed to run, before you want it to exit
         
    .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:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\AXBuild.exe" -Params "xppcompileall","/altbin=`"C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin`"","/aos=01","/dbserver=`"SQLTEST`"","/modelstore=`"AXTEST_model`"","/log=`"c:\temp\ax2012.tools\AxBuildLog`"","/compiler=`"C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\ax32serv.exe`""
         
        This will invoke the "C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\AXBuild.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 -OutputCommandOnly -Path "C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\AXBuild.exe" -Params "xppcompileall","/altbin=`"C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin`"","/aos=01","/dbserver=`"SQLTEST`"","/modelstore=`"AXTEST_model`"","/log=`"c:\temp\ax2012.tools\AxBuildLog`"","/compiler=`"C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\ax32serv.exe`""
         
        This will generate the command for the "C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\AXBuild.exe" executable.
        All parameters will be included in the output command.
         
    .EXAMPLE
        PS C:\> Invoke-Process -ShowOriginalProgress -Path "C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\AXBuild.exe" -Params "xppcompileall","/altbin=`"C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin`"","/aos=01","/dbserver=`"SQLTEST`"","/modelstore=`"AXTEST_model`"","/log=`"c:\temp\ax2012.tools\AxBuildLog`"","/compiler=`"C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\ax32serv.exe`""
         
        This will invoke the "C:\Program Files\Microsoft Dynamics AX\60\Server\AXTEST\Bin\AXBuild.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
        Author: M�tz Jensen (@Splaxi)
#>


function Invoke-Process {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName = "Generate")]
    param (
        [Parameter(Mandatory = $true)]
        
        [Alias('Executable')]
        [string] $Path,

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

        [int32] $TimeoutInMinutes = 0,

        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly,

        [switch] $EnableException
    )

    Invoke-TimeSignal -Start

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

    if (Test-PSFFunctionInterrupt) { return }

    [Int32] $millisecondFactor = 60000

    [Int32] $timeoutForExit = 0

    if ($TimeoutInMinutes -eq 0) {
        $timeoutForExit = [Int32]::MaxValue
    }
    else {
        $timeoutForExit = $TimeoutInMinutes * $millisecondFactor
    }

    $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) {
        $stdout = $p.StandardOutput.ReadToEnd()
        $stderr = $p.StandardError.ReadToEnd()
    }

    Write-PSFMessage -Level Verbose "Waiting for the $tool to complete"

    if(-not ($p.WaitForExit($timeoutForExit))) {
        Write-PSFMessage -Level Host "Timeout for exit has been <c='em'>reached</c>. Will execute a kill operation now."
        
        $p.Kill()

        Write-PSFMessage -Level Host "Standard output was: \r\n $stdout"
        Write-PSFMessage -Level Host "Error output was: \r\n $stderr"

        $messageString = "Stopping because Timeout has been reached."
        Stop-PSFFunction -Message "Stopping because of Timeout." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1
        return
    }
    
    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"
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Invoke timing logic
         
    .DESCRIPTION
        Invoke timing logic that keeps track of the time spend inside a function
         
    .PARAMETER Start
        Instruct the cmdlet that the starting of measurement
         
    .PARAMETER End
        Instruct the cmdlet that the ending of measurement
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -Start
         
        This will start the timing measurement.
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -End
         
        This will end the timing measurement and have the cmdlet write the details into the verbose log.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

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.IsPresent) {
        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"
            $Script:TimeSignals.Remove($Command)
        }
        else {
            Write-PSFMessage -Level Verbose -Message "The command '$Command' was never started to take part in time measurement."
        }
    }
}


<#
    .SYNOPSIS
        Create a new folder with datetime in its name
         
    .DESCRIPTION
        Create a new folder with current date and time as part of its name
         
    .PARAMETER Path
        Path to the parent folder where you want the new folder created
         
    .PARAMETER NoCreate
        Instruct the cmdlet not to create the folder
         
    .EXAMPLE
        PS C:\> New-FolderWithDateTime -Path "c:\temp\ax2012.tools"
         
        This will create a new folder with the current date and time as a child to the "c:\temp\ax2012.tools" folder.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

Function New-FolderWithDateTime {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $Path,

        [Parameter(Mandatory = $false, Position = 2)]
        [switch] $NoCreate
    )

    $dateString = (Get-Date).ToString("yyyy-MM-dd_HH.mm.ss")
    
    $backUpPath = Join-Path $Path $dateString
    
    Write-PSFMessage -Level Verbose -Message "Creating the new directory: $backUpPath" -Target $backUpPath
    
    if (-not ($NoCreate)) {
        $null = New-Item -Path $backUpPath -ItemType Directory -Force
    }
    
    $backUpPath
}


<#
    .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
         
    .PARAMETER DontBreak
        Instruct the cmdlet NOT to break execution whenever the test condition normally should
         
    .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
        Author: M�tz Jensen (@splaxi)
         
#>

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

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

        [switch] $Create,

        [switch] $ShouldNotExist,

        [switch] $DontBreak
    )
    
    $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 Verbose -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 Verbose -Message "Creating the path: $item" -Target $item
            $null = New-Item -Path $item -ItemType Directory -Force -ErrorAction Stop
            $temp = $true
        }
        elseif ($ShouldNotExist) {
            Write-PSFMessage -Level Verbose -Message "The should NOT exists: $item" -Target $item
        }
        elseif (-not $temp ) {
            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)) {
        if (-not $DontBreak) {
            Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1
        }
    }
    elseif ($arrList.Contains($true) -and $ShouldNotExist) {
        if (-not $DontBreak) {
            Stop-PSFFunction -Message "Stopping because file exists." -StepsUpward 1
        }
    }
    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
        Author: M�tz Jensen (@Splaxi)
         
#>

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
        Test PSBoundParameters whether or not to support TrustedConnection
         
    .DESCRIPTION
        Test callers PSBoundParameters (HashTable) for details that determines whether or not a SQL Server connection should support TrustedConnection or not
         
    .PARAMETER Inputs
        HashTable ($PSBoundParameters) with the parameters from the callers invocation
         
    .EXAMPLE
        PS C:\> $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters
         
        This will send the entire HashTable from the callers invocation, containing all explicit defined parameters to be analyzed whether or not the SQL Server connection should support TrustedConnection or not.
         
    .NOTES
        Author: M�tz Jensen (@splaxi)
         
#>

function Test-TrustedConnection {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [HashTable] $Inputs
    )

    if (($Inputs.ContainsKey("SqlUser")) -or ($Inputs.ContainsKey("SqlPwd"))) {
        Write-PSFMessage -Level Verbose -Message "Not capable of using Trusted Connection based on supplied SQL login details."
        $false
    }
    elseif ($Inputs.ContainsKey("TrustedConnection")) {
        Write-PSFMessage -Level Verbose -Message "The script was calling with TrustedConnection directly. This overrides all other logic in respect that the caller should know what it is doing. Value was: $($Inputs.TrustedConnection)" -Tag $Inputs.TrustedConnection
        $Inputs.TrustedConnection
    }
    else {
        $false
    }
}


<#
    .SYNOPSIS
        Update all the active variables
         
    .DESCRIPTION
        All PSF configuration entries that are names *.active* will be extracted to their corresponding $script:VARIABLENAMe
         
    .EXAMPLE
        PS C:\> Update-ActiveVariables
         
        This will update all the variables that maps directly to a PSF configuration value in the *.active* part of the configuration store.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
#>

function Update-ActiveVariables {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param ()
    
    foreach ($item in (Get-PSFConfig -FullName ax2012.tools.active*)) {
        $nameTemp = $item.FullName -replace "^ax2012.tools.", ""
        $name = ($nameTemp -Split "\." | ForEach-Object { (Get-Culture).TextInfo.ToTitleCase($_) } ) -Join ""
        
        New-Variable -Name $name -Value $item.Value -Scope Script -Force
    }
    
    if ([System.String]::IsNullOrEmpty(($Script:ActiveAosDatabaseserver -replace "null", "" ))) {
        Write-PSFMessage -Level Verbose -Message "ActiveAosDatabaseserver was empty. Defaulting to `"localhost`""
        $Script:ActiveAosDatabaseserver = "localhost"
    }
    
    if ([System.String]::IsNullOrEmpty(($Script:ActiveAosModelstoredatabase -replace "null", "" ))) {
        Write-PSFMessage -Level Verbose -Message "ActiveAosModelstoredatabase was empty. Defaulting to `"MicrosoftDynamicsAx_model`""
        $Script:ActiveAosModelstoredatabase = "MicrosoftDynamicsAx_model"
    }
    
    if ([System.String]::IsNullOrEmpty(($Script:ActiveAosDatabase -replace "null", "" ))) {
        Write-PSFMessage -Level Verbose -Message "ActiveAosDatabase was empty. Defaulting to `"MicrosoftDynamicsAx`""
        $Script:ActiveAosDatabase = "MicrosoftDynamicsAx"
    }
}


<#
    .SYNOPSIS
        Update the environment config variables
         
    .DESCRIPTION
        Update the active environment config variables that the module will use as default values
         
    .EXAMPLE
        PS C:\> Update-ActiveEnvironmentVariables
         
        This will update the environment variables.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
#>


function Update-ActiveEnvironmentVariables {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType()]
    param ( )
    
    $configName = (Get-PSFConfig -FullName "ax2012.tools.active.environment.config.name").Value

    if (([string]::IsNullOrEmpty($configName))) {
        return
    }

    $configName = $configName.ToString().ToLower()
    
    if (-not ($configName -eq "")) {
        $configHash = Get-AxActiveEnvironmentConfig -OutputAsHashtable
        foreach ($item in $configHash.Keys) {
            if ($item -eq "name") { continue }
            
            $name = "Environment" + (Get-Culture).TextInfo.ToTitleCase($item)
        
            Write-PSFMessage -Level Verbose -Message "$name - $($configHash[$item])" -Target $configHash[$item]
            Set-Variable -Name $name -Value $configHash[$item] -Scope Script
        }
    }
}


<#
    .SYNOPSIS
        Add configuration details for an entire AX 2012 environment
         
    .DESCRIPTION
        Build a configuration containing all the different servers / machines that is part of any given AX 2012 environment
         
        You could register your TEST, SAT, UAT, PROD environment and easily switch between them when you want to troubleshoot or run maintenance work against them
         
    .PARAMETER Name
        Name of the environment that you are adding
         
    .PARAMETER AosServers
        Array with server names of all the servers that host an AOS instance in the specific environment
         
    .PARAMETER InstanceName
        Name of the instance that is used to uniquely identify the environment across multiple AOS instances
         
    .PARAMETER DatabaseServers
        Array with server names of all the servers that host a SQL Server database for the environment
         
    .PARAMETER Database
        Database name for the SQL Server database that the AOS instance(s) connects to
         
    .PARAMETER ModelstoreDatabase
        Database name for the SQL Server database that holds the modelstore (code)
         
    .PARAMETER SsrsServers
        Array with server names of all the servers that host a Sql Server Reporting Services (SSRS) instance in the specific environment
         
    .PARAMETER EpServers
        Array with server names of all the servers that host a SharePoint installation with corresponding Enterprise Portal components in the specific environment
         
    .PARAMETER WmdpServers
        Array with server names of all the servers that host an IIS installation with corresponding Warehouse Mobile Device Portal (WMDP) components in the specific environment
         
    .PARAMETER Mr2012Servers
        Array with server names of all the servers that host a Management Reporter 2012 instance in the specific environment
         
    .PARAMETER SsasServers
        Array with server names of all the servers that host a Sql Server Analysis Services (SSAS) instance in the specific environment
         
    .PARAMETER Append
        Instructs the cmdlet to append the different parameter values with those that might already exist in the configuration store
         
    .PARAMETER Force
        Instruct the cmdlet to overwrite the specified parameter values in the configuration store
         
    .PARAMETER Temporary
        Instruct the cmdlet to only temporarily add the environment configuration in the configuration store
         
        Great help while building the configuration and you don't want to persist the configuration on the machine
         
    .EXAMPLE
        PS C:\> Add-AxEnvironmentConfig -Name AXTEST -AosServers TESTAOS01 -InstanceName AXTEST -DatabaseServers TESTSQL01 -Database AXTEST -ModelstoreDatabase AXTEST_model -Temporary
         
        This adds a new environment configuration to the configuration store.
        The Name AXTEST is used as the name for the configuration of the environment.
        The InstanceName AXTEST is used as the instance name for the configuration of the environment.
        The server TESTAOS01 is registered as the AOS Server.
        The server TESTSQL01 is registered as the SQL Server.
        The database AXTEST is registered as the SQL Server database.
        The database AXTEST_model is registered as the SQL Server database for the modelstore.
         
    .NOTES
        Tags: Environment, Config, Configuration, Servers
         
        Author: M�tz Jensen (@Splaxi)
         
    .LINK
        Clear-AxActiveEnvironmentConfig
         
    .LINK
        Get-AxActiveEnvironmentConfig
         
    .LINK
        Get-AxEnvironmentConfig
         
    .LINK
        Remove-AxEnvironmentConfig
         
    .LINK
        Set-AxActiveEnvironmentConfig
#>


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

        [string[]] $AosServers,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $InstanceName,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('DatabaseServer')]
        [string[]] $DatabaseServers,

        [Alias('DatabaseName')]
        [string] $Database,

        [string] $ModelstoreDatabase,

        [string[]] $SsrsServers,

        [string[]] $EpServers,

        [string[]] $WmdpServers,

        [string[]] $Mr2012Servers,

        [string[]] $SsasServers,

        [switch] $Append,

        [switch] $Force,

        [switch] $Temporary
    )
    
    Write-PSFMessage -Level Verbose -Message "Testing if configuration with the name already exists or not." -Target $configurationValue

    if (((Get-PSFConfig -FullName "ax2012.tools.environment.*.name").Value -contains $Name) -and (-not $Force) -and (-not $Append)) {
        $messageString = "An environment configuration with <c='em'>$Name</c> as name <c='em'>already exists</c>. If you want to <c='em'>overwrite</c> the current configuration, please supply the <c='em'>-Force</c> parameter. If you want to <c='em'>append</c> the current configuration, please supply the <c='em'>-Append</c> parameter"
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because an environment configuration already exists with that name." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
        return
    }

    $configName = $Name.ToLower()

    #The ':keys' label is used to have a continue inside the switch statement itself
    :keys foreach ($key in $PSBoundParameters.Keys) {
        
        $configurationValue = $PSBoundParameters.Item($key)
        $configurationName = $key.ToLower()
        $fullConfigName = ""

        Write-PSFMessage -Level Verbose -Message "Working on $key with $configurationValue" -Target $configurationValue
        
        switch ($key) {
            "Name" {
                $fullConfigName = "ax2012.tools.environment.$configName.name"
            }

            { "Temporary", "Force", "Append", "Verbose" -contains $_ } {
                continue keys
            }
            
            Default {
                $fullConfigName = "ax2012.tools.environment.$configName.$configurationName"
            }
        }

        if ($Append -and $key -ne "Name") {
            $oldValue = @(Get-PsfConfigValue -FullName $fullConfigName)
            $temp = @()

            if($null -ne $oldValue) {
                $temp += $oldValue
            }

            $temp += $configurationValue

            $configurationValue = $temp
        }

        Write-PSFMessage -Level Verbose -Message "Setting $fullConfigName to $configurationValue" -Target $configurationValue
        
        Set-PSFConfig -FullName $fullConfigName -Value $configurationValue
        
        if (-not $Temporary) { Register-PSFConfig -FullName $fullConfigName -Scope UserDefault }
    }
}


<#
    .SYNOPSIS
        Clear the active environment config
         
    .DESCRIPTION
        Clear the active environment config from the configuration store
         
    .PARAMETER Temporary
        Instruct the cmdlet to only temporarily clear the active environment configuration in the configuration store
         
    .EXAMPLE
        PS C:\> Clear-AxActiveEnvironmentConfig
         
        This will clear the active environment configuration from the configuration store.
         
    .NOTES
        Tags: Environment, Config, Configuration, Servers
         
        Author: M�tz Jensen (@Splaxi)
    .LINK
        Add-AxEnvironmentConfig
         
    .LINK
        Get-AxActiveEnvironmentConfig
         
    .LINK
        Get-AxEnvironmentConfig
         
    .LINK
        Remove-AxEnvironmentConfig
         
    .LINK
        Set-AxActiveEnvironmentConfig
#>


function Clear-AxActiveEnvironmentConfig {
    [CmdletBinding()]
    [OutputType()]
    param (
        [switch] $Temporary
    )

    $configurationName = "ax2012.tools.active.environment.config.name"
    
    Reset-PSFConfig -FullName $configurationName

    if (-not $Temporary) { Register-PSFConfig -FullName $configurationName -Scope UserDefault }
}


<#
    .SYNOPSIS
        Clear AX 2012 Client Cache Objects
         
    .DESCRIPTION
        Remove AX 2012 Client Cache Object files from the file system
         
    .PARAMETER ObjectType
        The type of cache object that you want to remove
         
        Valid options are:
        AUC
        KTI
        VSAssemblies
         
    .PARAMETER UserLocation
        Decide which user location that you want to work against
         
        Do you want to remove the cache objects from the current user or all users?
         
        Valid options are:
        CurrentUser
        AllUsers
         
    .PARAMETER ListOnly
        Instruct the cmdlet to only list the files that matches your selection from the other parameters
         
    .EXAMPLE
        PS C:\> Clear-AxClientCacheObjects -ObjectType "Auc" -UserLocation "CurrentUser" -ListOnly
         
        This will list all the Auc files under the current user location.
        It will work against the ObjectType "Auc".
        It will work againt the UserLocation "CurrentUser".
        It will only list the files and folders, it will NOT delete anything.
         
    .EXAMPLE
        PS C:\> Clear-AxClientCacheObjects -ObjectType "Auc" -UserLocation "CurrentUser"
         
        This will delete all the Auc files under the current user location.
        It will work against the ObjectType "Auc".
        It will work againt the UserLocation "CurrentUser".
         
        It WILL delete the files without further warning or notification!
         
    .EXAMPLE
        PS C:\> Clear-AxClientCacheObjects -ObjectType "Auc" -UserLocation "AllUsers"
         
        This will delete all the Auc files under all users locations.
        It will work against the ObjectType "Auc".
        It will work againt the UserLocation "AllUsers".
         
        It WILL delete the files without further warning or notification!
         
    .EXAMPLE
        PS C:\> Clear-AxClientCacheObjects -ObjectType "Auc","Kti","VSAssemblies" -UserLocation "CurrentUser"
         
        This will delete all the Auc,Kti and VSAssemblies files under the current user location.
        It will work against the ObjectType "Auc","Kti","VSAssemblies".
        It will work againt the UserLocation "CurrentUser".
         
        It WILL delete the files without further warning or notification!
         
    .NOTES
        Tags: Client Cache, Cache, KTI, AUC, VSAssemblies
         
        Author: M�tz Jensen (@Splaxi)
         
        All credits goes to Kenneth Madsen (@KennethGrupp) for providing detailed examples on how to achieve this the best way using powershell
         
#>


function Clear-AxClientCacheObjects {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    param (
        [string[]] $ObjectType,

        [string] $UserLocation,

        [switch] $ListOnly
    )
    
    Invoke-TimeSignal -Start

    $basePath = ""

    switch ($UserLocation) {
        "CurrentUser" {
            Write-PSFMessage -Level Verbose -Message "CurrentUser path was selected as the location to clear."

            $basePath = $env:LocalAppData
        }
        "AllUsers" {
            Write-PSFMessage -Level Verbose -Message "AllUsers path was selected as the location to clear."

            $basePath = "$env:LocalAppData" -replace "$env:username", "*"
        }
    }

    $pathToClear = ""
    foreach ($item in $ObjectType) {
        switch ($item.ToUpper()) {
            "AUC" {
                Write-PSFMessage -Level Verbose -Message "Working against cache object type: `"AUC`""

                $pathToClear = Join-Path $basePath "*.auc"
            }
            "KTI" {
                Write-PSFMessage -Level Verbose -Message "Working against cache object type: `"KTI`""

                $pathToClear = Join-Path $basePath "*.kti"
            }
            "VSASSEMBLIES" {
                Write-PSFMessage -Level Verbose -Message "Working against cache object type: `"VSAssemblies`""

                $pathToClear = Join-Path $basePath "Microsoft\Dynamics Ax\VSAssemblies*\*"
            }
        }

        Write-PSFMessage -Level Verbose -Message "Working against path: $pathToClear" -Target $pathToClear
        
        if ($ListOnly) {
            Get-ChildItem -Path $pathToClear
        }
        else {
            Remove-Item -Path $pathToClear -Force
        }
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Clear the Global Guid id from the AX 2012 database
         
    .DESCRIPTION
        Reset the Global Guid located in the SysSqmSettings table
         
        This guid (id) is used by the client to identify cache objects, so resetting this can be useful when troubleshooting
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER DatabaseName
        Name of the database
         
        Default value is: "MicrosoftDynamicsAx"
         
    .PARAMETER SqlUser
        User name of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER SqlPwd
        Password of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER OutputCommandOnly
        Instruct the cmdlet to only generate the needed command and not execute it
         
    .EXAMPLE
        PS C:\> Clear-AxInstanceGlobalGuid
         
        This will clear the current global guid in the AX 2012 database.
         
    .NOTES
        Tags:
         
        Author: M�tz Jensen (@Splaxi)
         
#>


function Clear-AxInstanceGlobalGuid {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName = "Generate")]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseName = $Script:ActiveAosDatabase,

        [string] $SqlUser,

        [string] $SqlPwd,

        [Parameter(ParameterSetName = "Generate")]
        [Switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start

    $baseParams = Get-DeepClone $PSBoundParameters
    $baseParams.Add("TrustedConnection", $true)

    $UseTrustedConnection = Test-TrustedConnection $baseParams

    $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd
    }

    $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\clear-axinstanceglobalguid.sql") -join [Environment]::NewLine

    if ($OutputCommandOnly) {
        (Get-SqlString $sqlCommand)
    }
    else {
        try {
            Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand)

            $sqlCommand.Connection.Open()
            $res = $sqlCommand.ExecuteNonQuery()

            Write-PSFMessage -Level Verbose -Message "SQL query executed." -Target $res
        }
        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 {
            if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
                $sqlCommand.Connection.Close()
            }
    
            $sqlCommand.Dispose()
        }
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Clear AX 2012 AOS Server Cache Objects
         
    .DESCRIPTION
        Remove AX 2012 AOS Server Cache Object files from the file system
         
    .PARAMETER ObjectType
        The type of cache object that you want to remove
         
        Valid options are:
        XppIL
        Label
        VSAssemblies
         
    .PARAMETER InstanceName
        Name of the instance that you are working against
         
        Default value can be configured with the Set-AxActiveAosConfig cmdlet
         
    .PARAMETER ListOnly
        Instruct the cmdlet to only list the files that matches your selection from the other parameters
         
    .EXAMPLE
        PS C:\> Clear-AxServerCacheObjects -ObjectType "XppIL" -InstanceName "AXTEST" -ListOnly
         
        This will list all the XppIL files under the AXTEST AOS Instance location.
        It will work against the ObjectType "XppIL".
        It will work againt the InstanceName "AXTEST".
        It will only list the files and folders, it will NOT delete anything.
         
    .EXAMPLE
        PS C:\> Clear-AxServerCacheObjects -ObjectType "XppIL" -InstanceName "AXTEST"
         
        This will delete all the XppIL files under the AXTEST AOS Instance location.
        It will work against the ObjectType "XppIL".
        It will work againt the InstanceName "AXTEST".
         
        It WILL delete the files without further warning or notification!
         
    .EXAMPLE
        PS C:\> Clear-AxServerCacheObjects -ObjectType "XppIL","Label","VSAssemblies" -InstanceName "AXTEST"
         
        This will delete all the XppIL,Label and VSAssemblies files under the AXTEST AOS Instance location.
        It will work against the ObjectType "XppIL","Label","VSAssemblies".
        It will work againt the InstanceName "AXTEST".
         
        It WILL delete the files without further warning or notification!
         
    .NOTES
        Tags: Client Cache, Cache, Label, XppIL, VSAssemblies
         
        Author: M�tz Jensen (@Splaxi)
         
        All credits goes to Kenneth Madsen (@KennethGrupp) for providing detailed examples on how to achieve this the best way using powershell
         
#>


function Clear-AxServerCacheObjects {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    param (
        [string[]] $ObjectType,

        [string] $InstanceName = $Script:ActiveAosInstancename,

        [switch] $ListOnly
    )
    
    if ([String]::IsNullOrEmpty($InstanceName)) {
        $messageString = "Instance name was <c='em'>empty</c>. You either need to supply it with <c='em'>-InstanceName</c> parameter or configure it using the <c='em'>Set-AxActiveAosConfig</c> cmdlet."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because of missing instance name parameter." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
        return
    }

    if ($InstanceName -eq "*") {
        $messageString = "Instance name cannot be <c='em'>*</c>. You either need to supply an instance name that will only result in <c='em'>one</c> AOS instance."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because of missing instance name parameter." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
        return
    }

    Invoke-TimeSignal -Start

    $basePath = (Get-AxAosInstance -InstanceName $InstanceName).BinDirectory

    $parms = @{}
    foreach ($item in $ObjectType) {
        switch ($item.ToUpper()) {
            "XPPIL" {
                Write-PSFMessage -Level Verbose -Message "Working against cache object type: `"XppIL`""

                $parms.Path = (Join-Path $basePath "XppIL")
                $parms.Include = "*.*"
            }
            "LABEL" {
                Write-PSFMessage -Level Verbose -Message "Working against cache object type: `"Label`""
                
                $parms.Path = (Join-Path $basePath "Application\appl\standard\ax*.*")
            }
            "VSASSEMBLIES" {
                Write-PSFMessage -Level Verbose -Message "Working against cache object type: `"VSAssemblies`""
                
                $parms.Path = (Join-Path $basePath "VSAssemblies\*.*")
            }
        }

        Write-PSFMessage -Level Verbose -Message "Working against path: $($parms.Path)" -Target $parms.Path
        
        if ($ListOnly) {
            Get-ChildItem @parms -Recurse
        }
        else {
            Remove-Item @parms -Recurse -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
        }
    }
    
    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Export an AX 2012 modelstore file
         
    .DESCRIPTION
        Export an AX 2012 modelstore file from the modelstore database
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER ModelstoreDatabase
        Name of the modelstore database
         
        Default value is: "MicrosoftDynamicsAx_model"
         
        Note: From AX 2012 R2 and upwards you need to provide the full name for the modelstore database. E.g. "AX2012R3_PROD_model"
         
    .PARAMETER InstanceName
        Name of the instance that you are working against
         
        If not supplied the cmdlet will take the name of the database and use that
         
    .PARAMETER Suffix
        A suffix text value that you want to add to the name of the file while it is exported
         
        The default value is: (Get-Date).ToString("yyyyMMdd")
         
        This will always name you file with the current date
         
    .PARAMETER Path
        Path to the location where you want the file to be exported
         
        Default value is: "c:\temp\ax2012.tools"
         
    .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 generate the needed command and not execute it
         
    .EXAMPLE
        PS C:\> Export-AxModelStoreV2
         
        This will execute the cmdlet with all the default values.
        This will work against the SQL server that is on localhost.
        The database is expected to be "MicrosoftDynamicsAx_model".
        The path where the exported modelstore file will be saved is: "c:\temp\ax2012.tools".
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Export-AxModelStoreV2 {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName="Generate")]
    Param(
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [string] $ModelstoreDatabase = $Script:ActiveAosModelstoredatabase,

        [string] $InstanceName = $Script:ActiveAosInstancename,

        [string] $Suffix = $((Get-Date).ToString("yyyyMMdd")),
        
        [string] $Path = $Script:DefaultTempPath,

        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start

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

    if (-not (Test-PathExists -Path $Script:AxPowerShellModule -Type Leaf)) { return }
        
    if ([System.String]::IsNullOrEmpty($InstanceName)) {
        $InstanceName = "{0}" -f $ModelstoreDatabase.Replace("_model", "")
    }

    if (-not ([system.string]::IsNullOrEmpty($Suffix))) {
        $ExportPath = Join-Path $Path "$($InstanceName)_$($Suffix).axmodelstore"
    }
    else {
        $ExportPath = Join-Path $Path "$InstanceName.axmodelstore"
    }

    $params = @{
        Server   = $DatabaseServer
        Database = $ModelstoreDatabase
        File     = $ExportPath
    }

    if ($OutputCommandOnly) {
        $arguments = Convert-HashToArgString -InputObject $params

        "Export-AxModelStore $($arguments -join ' ')"
    }
    else {
        Write-PSFMessage -Level Verbose -Message "Starting the export of the model store"
        
        $null = Import-Module $Script:AxPowerShellModule

        $outputRes = Export-AxModelStore @params

        if ($ShowOriginalProgress) {
            $outputRes
        }

        Clear-Ax2012StandardPowershellModule
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Export AX 2012 model
         
    .DESCRIPTION
        Export AX 2012 model from the AX 2012 model store
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER ModelstoreDatabase
        Name of the modelstore database
         
        Default value is: "MicrosoftDynamicsAx_model"
         
        Note: From AX 2012 R2 and upwards you need to provide the full name for the modelstore database. E.g. "AX2012R3_PROD_model"
         
    .PARAMETER Path
        Path to the location where you want the file to be exported
         
        Default value is: "c:\temp\ax2012.tools"
         
    .PARAMETER Name
        Name of the AX 2012 model that you are looking for
         
        Accepts wildcards for searching. E.g. -Name "ISV*MODULE*"
         
        Default value is "*" which will search for all models
         
    .PARAMETER Id
        Id of the AX 2012 model that you are looking for
         
        Accepts wildcards for searching. E.g. -Id "2*"
         
        Default value is "*" which will search for all models
         
    .PARAMETER Layer
        Layer where the AX 2012 model that you are looking for should reside
         
        Accepts wildcards for searching. E.g. -Layer "IS*"
         
        Default value is "*" which will search for models in all layers
         
    .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 output the script to execute the command in hand
         
    .EXAMPLE
        PS C:\> Get-AxAosInstance | Export-AxModelV2
         
        This will fetch all the AX 2012 AOS instances that are configured on the machine.
        Foreach of the instances it will export all AX 2012 Models into a sub folder to "c:\temp\ax2012.tools".
         
    .EXAMPLE
        PS C:\> Export-AxModelV2 -DatabaseServer localhost -ModelstoreDatabase MicrosoftDynamicsAx_model -Name *CUS*
         
        This will fetch all the AX 2012 AOS instances that are configured on the machine.
        Foreach of the instances it will export all AX 2012 Models into a sub folder to "c:\temp\ax2012.tools".
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Export-AxModelV2 {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName="Generate")]
    Param(
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $ModelstoreDatabase = $Script:ActiveAosModelstoredatabase,
        
        [string] $Path = $Script:DefaultTempPath,

        [string] $Name = "*",

        [string] $Id = "*",

        [string] $Layer = "*",

        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly
    )

    BEGIN {
        if (-not ($OutputCommandOnly)) {
            if (-not (Test-PathExists -Path $Path -Type Container -Create)) { return }
            $backupFilePath = New-FolderWithDateTime -Path $Path
        }
        else {
            $backupFilePath = New-FolderWithDateTime -Path $Path -NoCreate
        }

        $null = Import-Module $Script:AxPowerShellModule
    }

    PROCESS {
        Invoke-TimeSignal -Start

        $xml = (Get-AXModel -Server $DatabaseServer -Database $ModelstoreDatabase | ConvertTo-Xml )
        $nodes = $xml.SelectNodes("Objects/Object")

        foreach ($obj in $nodes) {
            $filenameAxModel = ""
            $modelId = ""
            $modelLayer = ""
            $modelName = ""

            # Loop all properties
            foreach ($property in $obj.SelectNodes("Property")) {
                if ($property.GetAttribute("Name").Equals( "Name" )) {
                    $modelName = $property.InnerText
                }
        
                if ($property.GetAttribute("Name").Equals( "Layer" )) {
                    $modelLayer = $property.InnerText
                }
        
                if ($property.GetAttribute("Name").Equals( "ModelId" )) {
                    $modelId = $property.InnerText
                }
            }
    
            $filenameAxModel = $modelId + "_" + $modelName + ".axmodel"

            Write-PSFMessage -Level Verbose -Message "Testing that ModelName, ModelId and Layer matches the search criteria"
            if ($modelName -NotLike $Name) { continue }
            if ($modelId -NotLike $Id) { continue }
            if ($modelLayer -NotLike $Layer) { continue }

            if ($Script:LayerDictionary.ContainsKey($modelLayer.ToUpper()) -and $filenameAxModel -ne "") {
                $localLayer = $Script:LayerDictionary.Get_Item($modelLayer.ToUpper()) + $modelLayer.ToUpper()
                $tempPath = Join-Path $backupFilePath $localLayer
        
                $filenameAxModel = Join-Path $tempPath $filenameAxModel

                $params = @{Model = $modelName; File = $filenameAxModel;
                    Server = $DatabaseServer; Database = $ModelstoreDatabase
                }

                if ($OutputCommandOnly) {
                    $arguments = Convert-HashToArgString -InputObject $params

                    "Export-AXModel $($arguments -join ' ')"
                }
                else {
                    if (-not (Test-PathExists -Path $tempPath -Type Container -Create)) { return }

                    Write-PSFMessage -Level Verbose -Message "Starting the export of the ax model file"
                    $outputRes = Export-AXModel @params

                    if ($ShowOriginalProgress) {
                        $outputRes
                    }
                }
            }
            else {
                Write-PSFMessage -Level Verbose -Message "Skipping $filenameAxModel in layer $modelLayer"
            }
        }

        Invoke-TimeSignal -End
    }

    END {
        if (-not ($OutputCommandOnly)) {
            [PSCustomObject]@{
                Path = $backupFilePath
            }
        }

        Clear-Ax2012StandardPowershellModule
    }
}


<#
    .SYNOPSIS
        Get the active stored AX 2012 AOS configuration
         
    .DESCRIPTION
        Get the active AX 2012 AOS configuration from the configuration storage
         
    .EXAMPLE
        PS C:\> Get-AxActiveAosConfig
         
        This will export all the stored details saved into the configuration storage.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxActiveAosConfig {
    [CmdletBinding()]
    param (
        
    )

    $res = [Ordered]@{}

    $res.InstanceName = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.instancename'
    $res.BinDirectory = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.bindirectory'

    $res.DatabaseServer = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.databaseserver'
    $res.DatabaseName = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.database'
    $res.ModelstoreDatabase = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.modelstoredatabase'

    $res.AosPort = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.aos.port'
    $res.WsdlPort = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.wsdl.port'
    $res.NetTcpPort = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.nettcp.port'
    
    $res.InstanceNumber = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.instance.number'
    $res.ComputerName = Get-PSFConfigValue -FullName 'ax2012.tools.active.aos.computername'

    [PSCustomObject] $res
}


<#
    .SYNOPSIS
        Get the active environment configuration
         
    .DESCRIPTION
        Get the active environment configuration from the configuration store
         
    .PARAMETER OutputAsHashtable
        Instruct the cmdlet to return a hashtable object
         
    .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:\> Get-AxActiveEnvironmentConfig
         
        This will get the active environment configuration.
         
    .NOTES
        Tags: Environment, Config, Configuration, Servers
         
        Author: M�tz Jensen (@Splaxi)
         
    .LINK
        Add-AxEnvironmentConfig
         
    .LINK
        Get-AxEnvironmentConfig
         
    .LINK
        Set-AxActiveEnvironmentConfig
#>


function Get-AxActiveEnvironmentConfig {
    [CmdletBinding()]
    [OutputType()]
    param (
        [switch] $OutputAsHashtable,

        [switch] $EnableException
    )

    $configName = (Get-PSFConfig -FullName "ax2012.tools.active.environment.config.name").Value

    if ($configName -eq "") {
        $messageString = "It looks like there <c='em'>isn't configured</c> an active environment configuration."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because an active environment configuration wasn't found." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
        return
    }

    Get-AxEnvironmentConfig -Name $configName -OutputAsHashtable:$OutputAsHashtable
}


<#
    .SYNOPSIS
        Get AX 2012 AOS Instance
         
    .DESCRIPTION
        Get AX 2012 AOS Instance details from the local machine
         
    .PARAMETER Name
        The search string to filter the AOS instance that you're looking for
         
        The parameter supports wildcards. E.g. -Name "*DEV*"
         
        Default value is "*" and will give you all the instances
         
    .PARAMETER InstanceNo
        The search string to filter the AOS instance that you're looking for
         
        The parameter supports wildcards. E.g. -InstanceNo "*1*"
         
        Default value is "*" and will give you all the instances
         
    .EXAMPLE
        PS C:\> Get-AxAosInstance
         
        This will get you all the installed AX 2012 AOS instances on the machine
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxAosInstance {
    [CmdletBinding()]
    Param(

        [Alias('InstanceName')]
        [string] $Name = "*",
        
        [string] $InstanceNo = "*"
    )
    $Instances = Get-ChildItem -Path $Script:RegistryAos
        
    $res = New-Object System.Collections.ArrayList
    
    $Instances | ForEach-Object {
        $null = $res.Add((Get-AxAosInstanceDetails $_.Name))
    }

    foreach ($obj in $res) {
        if ($obj.InstanceName -NotLike $Name) { continue }
        if ($obj.InstanceNumber -NotLike $InstanceNo) { continue }

        $obj
    }
}


<#
    .SYNOPSIS
        Get AOS server details from the SQL Server database
         
    .DESCRIPTION
        Traverse the SysServerConfig table from the AX 2012 database and get all registered AOS Servers
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER DatabaseName
        Name of the database
         
        Default value is: "MicrosoftDynamicsAx"
         
    .PARAMETER SqlUser
        User name of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER SqlPwd
        Password of the SQL Server credential that you want to use when working against the database
         
    .EXAMPLE
        PS C:\> Get-AxAosServerFromDB
         
        This will query the "MicrosoftDynamicsAx" hosted on the localhost server.
        It will extract all AOS Servers from the SysServerConfig table.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxAosServerFromDB {
    [CmdletBinding()]
    #[OutputType()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 1)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)]
        [string] $DatabaseName = $Script:ActiveAosDatabase,

        [Parameter(Mandatory = $false, Position = 3)]
        [string] $SqlUser,

        [Parameter(Mandatory = $false, Position = 4)]
        [string] $SqlPwd
    )

    Invoke-TimeSignal -Start

    $baseParams = Get-DeepClone $PSBoundParameters
    $baseParams.Add("TrustedConnection", $true)

    $UseTrustedConnection = Test-TrustedConnection $baseParams

    $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd
    }

    $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-aosserversfromdatabase.sql") -join [Environment]::NewLine

    try {
        Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand)

        $sqlCommand.Connection.Open()

        $reader = $sqlCommand.ExecuteReader()

        while ($reader.Read() -eq $true) {
            $rawServer = "$($reader.GetString($($reader.GetOrdinal("SERVERID"))))"
            
            [PSCustomObject]@{
                RawServerName  = $rawServer
                InstanceNumber = $rawServer.ToString().Split("@")[0]
                ServerName     = $rawServer.ToString().Split("@")[1]
                IsBatchEnabled = [bool][int]"$($reader.GetInt32($($reader.GetOrdinal("IsBatch"))))"
            }
        }
    }
    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 {
        if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
            $sqlCommand.Connection.Close()
        }
    
        $sqlCommand.Dispose()
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Get a valid Autorun XML file for AX 2012
         
    .DESCRIPTION
        Generate a valid Autorun XML file based on the selected parameters and have it saved on the disk
         
    .PARAMETER OutputPath
        The path where you want to store to autorun xml file
         
        The path must be a full path to a file
         
    .PARAMETER LogFile
        The path where you want the autorun process to save its log file
         
        The path must be a full path to a file
         
    .PARAMETER ExitWhenDone
        Instruct the autorun process wether you want it to exit the AX 2012 client when done or not
         
    .PARAMETER SynchronizeDB
        Instruct the autorun to do a synchronization of the database
         
    .PARAMETER CompileCilFull
        Instruct the autorun to do a full CIL compile
         
    .PARAMETER CompileCilIncremental
        Instruct the autorun to do a incremental CIL compile
         
    .PARAMETER CompileXpp
        Instruct the autorun to do a full X++ (Xpp) compile
         
    .PARAMETER CompileXppAndXRefUpdate
        Instruct the autorun to do a full X++ (Xpp) compile and do a full Cross Reference (XRef) update
         
    .EXAMPLE
        PS C:\> Get-AxAutoRunXml -ExitWhenDone -SynchronizeDB
         
        This will generate a valid Autorun xml file for synchronizing the database.
        It will use the default path "c:\temp\ax2012.tools\Autorun.xml" where the file will be stored.
        It will use the default path "c:\temp\ax2012.tools\Autorun_Output.txt" to instruct the Autorun where to save its log file.
        It will instruct the Autorun to exit when done.
        It will instruct the Autorun to do a synchronization of the database.
         
    .NOTES
        Tags:
        Author: M�tz Jensen (@Splaxi)
#>


function Get-AxAutoRunXml {
    [CmdletBinding()]
    param (

        [String] $OutputPath = (Join-Path $Script:DefaultTempPath "Autorun.xml"),

        [String] $LogFile = (Join-Path $Script:DefaultTempPath "Autorun_Output.txt"),

        [Switch] $ExitWhenDone,

        [Switch] $SynchronizeDB,

        [Switch] $CompileCilFull,

        [Switch] $CompileCilIncremental,
        
        [Switch] $CompileXpp,

        [Switch] $CompileXppAndXRefUpdate
        
    )

    if ([System.IO.Path]::HasExtension($OutputPath)) {
        $parentPath = Split-Path -Path $OutputPath -Parent

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

        if (Test-PSFFunctionInterrupt) { return }
    }

    $actions = New-Object System.Collections.Generic.List[string]
    
    if ($SynchronizeDB) {
        Write-PSFMessage -Level Verbose -Message "SynchronizeDB was added to the action list." -Target $SynchronizeDB
        $actions.Add("SynchronizeDB")
    }
    
    if ($CompileCilFull -or $CompileCilIncremental) {
        
        if ($CompileCilFull) {
            Write-PSFMessage -Level Verbose -Message "CompileCilFull was added to the action list." -Target $CompileCilFull
            $actions.Add("CompileCilFull")
        }
        else {
            Write-PSFMessage -Level Verbose -Message "CompileCilIncremental was added to the action list." -Target $CompileCilIncremental
            $actions.Add("CompileCilIncremental")
        }
    }

    if ($CompileXppAndXRefUpdate -or $CompileXpp) {
        
        if ($CompileXppAndXRefUpdate) {
            Write-PSFMessage -Level Verbose -Message "CompileXppAndXRefUpdate was added to the action list." -Target $CompileXppAndXRefUpdate
            $actions.Add("CompileXppAndXRef")
        }
        else {
            Write-PSFMessage -Level Verbose -Message "CompileXpp was added to the action list." -Target $CompileXpp
            $actions.Add("CompileXpp")
        }
    }

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

    foreach ($item in $actions.ToArray()) {
        $autoRunXml.Add($(Get-AutoRunXML -Action $item))
    }

    $resXml = (Get-Content "$script:ModuleRoot\internal\xml\autotemplate.xml") -join [Environment]::NewLine

    $resXml = $resXml.Replace("{{ExitWhenDone}}", $ExitWhenDone.ToBool().ToString())
    $resXml = $resXml.Replace("{{LogPath}}", $LogFile)

    $resXml = $resXml.Replace("{{Action}}", $($autoRunXml.ToArray() -join [Environment]::NewLine))

    $resXml | Out-File -FilePath $OutputPath -Encoding utf8 -Force

    [PSCustomObject]@{
        Path = $OutputPath
    }
}


<#
    .SYNOPSIS
        Get the build numbers
         
    .DESCRIPTION
        Get the build numbers for the AX 2012 client
         
    .PARAMETER Path
        The path to the Ax32.exe file you want to work against
         
        The default path is read from the registry
         
    .EXAMPLE
        PS C:\> Get-AxClientBuild
         
        This will get the executable path and the build numbers for the client.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxClientBuild {
    [CmdletBinding()]
    Param(
        [string] $Path = $(Join-Path $Script:ClientBin "Ax32.exe")
    )
    
    $BuildNumbers = Get-FileVersion -Path $Path

    $clientDetails = [Ordered]@{}

    $clientDetails.ExecutablePath = $Path

    $clientDetails.FileVersion = $BuildNumbers.FileVersion
    $clientDetails.ProductVersion = $BuildNumbers.ProductVersion
    $clientDetails.FileVersionUpdated = $BuildNumbers.FileVersionUpdated
    $clientDetails.ProductVersionUpdated = $BuildNumbers.ProductVersionUpdated

    [PSCustomObject] $clientDetails
}


<#
    .SYNOPSIS
        Get the AX 2012 client configuration
         
    .DESCRIPTION
        Get the AX 2012 client configuration from the registry
         
    .PARAMETER Name
        Name of the configuration that you are looking for
         
        The parameter supports wildcards. E.g. -Name "*DEV*"
         
    .EXAMPLE
        PS C:\> Get-AxClientConfig
         
        This will get all available client configurations from the registry and display them.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxClientConfig {
    [CmdletBinding()]
    Param(
        [Parameter()]
        [SupportsWildcards()]
        [string] $Name = "*"
    )
    
    $configs = Get-ChildItem -Path $Script:RegistryClientConfig

    foreach ($item in $configs) {
        $RegistryPath = Get-Item -Path $($item.Name.Replace("HKEY_CURRENT_USER", "HKCU:"))
        $configName = Split-Path -Path $RegistryPath.Name -Leaf
        
        if ($configName -NotLike $Name) { continue }

        $RegInner = Get-ItemProperty -Path $($RegistryPath.Name.Replace("HKEY_CURRENT_USER", "HKCU:"))
        $res = [Ordered]@{ConfigName = $configName}
        
        $res.AosServer = ($RegInner.Aos2).Split(":")[0]
        $res.AosPort = ($RegInner.Aos2).Split(":")[1]
        
        if ($RegInner.Aos2 -like "*@*") {
            $res.InstanceName = ($RegInner.Aos2).Split("@")[0]
        }

        $res.AosTrafficEncrypted = $RegInner.AosEncryption

        $res.BinDir = $RegInner.BinDir
        $res.ClientDirectory = $RegInner.Directory
        $res.Aol = $RegInner.Aol
        $res.AolCode = $RegInner.AolCode
        
        [PSCustomObject]$res
    }
}


<#
    .SYNOPSIS
        Get AX 2012 environment details from the configuration store
         
    .DESCRIPTION
        Get the environment details for the AX 2012 environment(s) that are stored in the configuration store
         
    .PARAMETER Name
        Name of the configuration that you want to work against
         
    .PARAMETER OutputAsHashtable
        Instruct the cmdlet to return a hastable object
         
    .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:\> Get-AxEnvironmentConfig
         
        This will get all saved environment configurations.
         
    .NOTES
        Tags: Servicing, Environment, Config, Configuration, Servers
         
        Author: M�tz Jensen (@Splaxi)
         
    .LINK
        Add-AxEnvironmentConfig
         
    .LINK
        Get-AxActiveEnvironmentConfig
         
    .LINK
        Set-AxActiveEnvironmentConfig
         
#>


function Get-AxEnvironmentConfig {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
    [CmdletBinding()]
    [OutputType('PSCustomObject')]
    param (
        [string] $Name = "*",

        [switch] $OutputAsHashtable,

        [switch] $EnableException
    )
    
    Write-PSFMessage -Level Verbose -Message "Fetch all configurations based on $Name" -Target $Name

    $Name = $Name.ToLower()
    $configurations = Get-PSFConfig -FullName "ax2012.tools.environment.$Name.name"

    if($($configurations.count) -lt 1) {
        $messageString = "No configurations found <c='em'>with</c> the name."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because no configuration found." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>','')))
        return
    }
    
    foreach ($configName in $configurations.Value.ToLower()) {
        Write-PSFMessage -Level Verbose -Message "Working against the $configName configuration" -Target $configName
        $res = @{}

        $configName = $configName.ToLower()

        foreach ($config in Get-PSFConfig -FullName "ax2012.tools.environment.$configName.*") {
            Write-PSFMessage -Level Verbose -Message "Working against the $($config.FullName.ToString()) of $configName configuration" -Target $configName

            $propertyName = (Get-Culture).TextInfo.ToTitleCase($config.FullName.ToString().Replace("ax2012.tools.environment.$configName.", "")).Replace("servers","Servers")
            $res.$propertyName = $config.Value
        }
        
        if($OutputAsHashtable) {
            $res
        } else {
            [PSCustomObject]$res | Select-PSFObject -TypeName "AX2012.TOOLS.Environment.Config"
        }
    }
}


<#
    .SYNOPSIS
        Get the registered details for Azure Logic App
         
    .DESCRIPTION
        Get the details that are stored for the module when
        it has to invoke the Azure Logic App
         
    .EXAMPLE
        PS C:\> Get-AxLogicAppConfig
         
        This will fetch the current registered Azure Logic App details on the machine.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxLogicAppConfig {
    [CmdletBinding()]
    param ()

    [PSCustomObject][ordered]@{
        Email = (Get-PSFConfigValue -Fullname 'ax2012.tools.active.logicapp.email')
        URL = (Get-PSFConfigValue -Fullname 'ax2012.tools.active.logicapp.url')
        Subject = (Get-PSFConfigValue -Fullname 'ax2012.tools.active.logicapp.subject')
    }
}


<#
    .SYNOPSIS
        Get Management Reporter 2012 Instance
         
    .DESCRIPTION
        Get Management Reporter 2012 Instance details from the local machine
         
    .EXAMPLE
        PS C:\> Get-AxMr2012Instance
         
        This will output all the core Management Reporter details on the machine.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxMr2012Instance {
    [CmdletBinding()]
    Param()
    
    $RegistryPath = $Script:RegistryMr2012
    $RegKey = Get-Item -Path $RegistryPath.Replace("HKEY_LOCAL_MACHINE", "HKLM:")
    $RegOuter = Get-ItemProperty -Path ($RegKey.Name).Replace("HKEY_LOCAL_MACHINE", "HKLM:")

    $res = [Ordered]@{ }

    $res.BinDirectory = Join-Path $RegOuter.InstallLocation Server

    if (Test-PathExists -Path $Script:Mr2012DeploymentLogsPath -Type Container) {
        $res.DeploymentLogsPath = $Script:Mr2012DeploymentLogsPath
    }

    $configPath = Join-Path $res.BinDirectory $Script:Mr2012ConfigPath
    $serviceConfigPath = Join-Path $res.BinDirectory $Script:Mr2012ServiceConfigPath

    if (-not (Test-PathExists -Path $configPath, $serviceConfigPath -Type Leaf)) { return }

    if (Test-PSFFunctionInterrupt) { return }

    $configXmlString = Get-Content -Path $configPath

    $configXmlString -split ";" | ForEach-Object {
        $tempArray = $_.ToString().Split("=")

        switch -Wildcard  ($_) {
            "*Data Source=*" {
                $res.DatabaseServer = $tempArray[$tempArray.Length - 1]
            }
            "*Initial Catalog=*" {
                $res.Database = $tempArray[$tempArray.Length - 1]
                
            }
        }
    }

    $serviceConfigXmlString = Get-Content -Path $serviceConfigPath

    $serviceConfigXmlString -split "<" | ForEach-Object {
        $tempArray = $_.ToString().Split("=")

        switch -Wildcard  ($_) {
            "*DefaultHttpBaseAddress*" {
                $tempString = $tempArray[$tempArray.Length - 1]
                
                $res.HttpAddress = $tempString.Substring(0, $tempString.LastIndexOf('"')) -replace '"', ''
            }
        }
    }

    [PSCustomObject]$res | Select-PSFObject -TypeName "AX2012.TOOLS.Mr2012.Config"
}


<#
    .SYNOPSIS
        Get the parameter sniffing configuration
         
    .DESCRIPTION
        Get the parameter sniffing value from the database that has been released by Microsoft for AX 2012
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER DatabaseName
        Name of the database
         
        Default value is: "MicrosoftDynamicsAx"
         
    .PARAMETER SqlUser
        User name of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER SqlPwd
        Password of the SQL Server credential that you want to use when working against the database
         
    .EXAMPLE
        PS C:\> Get-AxParameterSniffingSetting
         
        This will query the database for the parameter sniffing settings.
        The result will be displayed along with a ShouldBe value to easily tell you if something is off.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxParameterSniffingSetting {
    [CmdletBinding()]
    #[OutputType()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 1)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)]
        [string] $DatabaseName = $Script:ActiveAosDatabase,

        [Parameter(Mandatory = $false, Position = 3)]
        [string] $SqlUser,

        [Parameter(Mandatory = $false, Position = 4)]
        [string] $SqlPwd
    )

    Invoke-TimeSignal -Start

    $baseParams = Get-DeepClone $PSBoundParameters
    $baseParams.Add("TrustedConnection", $true)

    $UseTrustedConnection = Test-TrustedConnection $baseParams

    $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd
    }

    $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-axparametersniffingsetting.sql") -join [Environment]::NewLine

    try {
        Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand)

        $sqlCommand.Connection.Open()

        $reader = $sqlCommand.ExecuteReader()

        while ($reader.Read() -eq $true) {
            [PSCustomObject]@{
                Name = "$($reader.GetString($($reader.GetOrdinal("Name"))))"
                Value = "$($reader.GetString($($reader.GetOrdinal("Value"))))"
                ShouldBe = "1"
            }
        }
    }
    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 {
        if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
            $sqlCommand.Connection.Close()
        }
    
        $sqlCommand.Dispose()
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Get hash values for XppIL files
         
    .DESCRIPTION
        Generate the file hash values for each *.netmodule & *.dll file in the XppIL folder
         
    .PARAMETER InstanceName
        Name of the instance that you are working against
         
        Default value can be configured with the Set-AxActiveAosConfig cmdlet
         
    .PARAMETER OutputPath
        Path to the folder where the output file must be saved
         
        Default value is: "C:\temp\ax2012.tools"
         
    .PARAMETER FileName
        Name of the output as you want it to be named
         
        If left empty the output file will be named based on the following pattern: "SERVERNAME_INSTANCENAME_XppIL_HashValue.txt"
         
    .EXAMPLE
        PS C:\> Get-AxServerXppILHash -InstanceName "AXTEST"
         
        This will generate the file hash values for the XppIL files for the AXTEST AOS Instance.
        It will work against the Instance AXTEST.
        It will save the output file to the default folder location "C:\temp\ax2012.tools".
        The file will named "SERVER_AXTEST_XppIL_HashValues.txt"
         
    .EXAMPLE
        PS C:\> Get-AxServerXppILHash
         
        This will generate the file hash values for the XppIL files for the default AOS Instance.
        It will work against the default AOS Instance that has been configured with Set-AxActiveAosConfig.
        It will save the output file to the default folder location "C:\temp\ax2012.tools".
        The file will named "SERVER_INSTANCENAME_XppIL_HashValues.txt"
         
        The default value of the instance name can be configured with the Set-AxActiveAosConfig cmdlet.
         
    .NOTES
        Tags: XppIL, Hash, Files
         
        Author: M�tz Jensen (@Splaxi)
         
        All credits goes to Kenneth Madsen (@KennethGrupp) for providing detailed examples on how to achieve this the best way using powershell
         
#>


function Get-AxServerXppILHash {
    [CmdletBinding()]
    param (
        [string] $InstanceName = $Script:ActiveAosInstancename,

        [string] $OutputPath = $Script:DefaultTempPath,

        [string] $FileName
    )
    
    if ([String]::IsNullOrEmpty($InstanceName)) {
        $messageString = "Instance name was <c='em'>empty</c>. You either need to supply it with <c='em'>-InstanceName</c> parameter or configure it using the <c='em'>Set-AxActiveAosConfig</c> cmdlet."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because of missing instance name parameter." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
        return
    }

    if ($InstanceName -eq "*") {
        $messageString = "Instance name cannot be <c='em'>*</c>. You either need to supply an instance name that will only result in <c='em'>one</c> AOS instance."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because of missing instance name parameter." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
        return
    }

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

    Invoke-TimeSignal -Start

    if ([String]::IsNullOrEmpty($FileName)) {
        $filename = "$($env:COMPUTERNAME)_$InstanceName`_XppIL_HashValues.txt"
    }
    
    $outputFile = Join-Path $OutputPath $FileName

    Write-PSFMessage -Level Verbose -Message "Removing old output file: $outputFile" -Target $outputFile
    Remove-Item -Path $outputFile -Force -WarningAction SilentlyContinue -ErrorAction SilentlyContinue

    $basePath = (Get-AxAosInstance -InstanceName $InstanceName).BinDirectory

    $fileExtensions = @("*.netmodule", "*.dll")

    $searchPath = (Join-Path $basePath "XppIL")

    Write-PSFMessage -Level Verbose -Message "Working against the $InstanceName instance with the path: $searchPath" -Target $searchPath
    $xppFiles = Get-ChildItem -Path $searchPath -Include $fileExtensions -Recurse
    
    $resList = New-Object System.Collections.Generic.List[object]

    foreach ($item in $xppFiles) {
        $resList.Add($(Get-FileHash -Path $item.FullName))
    }

    Write-PSFMessage -Level Verbose -Message "Generating the hash values and saving the output to path: $outputFile" -Target $outputFile
    $resList.ToArray() | Sort-Object Path | Format-Table Hash, Path -Wrap -AutoSize | Out-String -Width 4000 | Out-File $outputFile -Encoding utf8

    [PSCustomObject]@{
        Path = $outputFile
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Get the status of the AX 2012 service(s)
         
    .DESCRIPTION
        Get the status of AX 2012 service(s) on the computer
         
    .PARAMETER ComputerName
        Name of the computer(s) that you want to work against
         
    .PARAMETER All
        Instruct the cmdlet to include all known AX 2012 services
         
    .PARAMETER AosInstanceName
        Name of the AOS instance that you are looking for
         
        Accepts wildcards for searching. E.g. -AosInstanceName "*DEV*"
         
        If AxActiveAosConfiguration has been configured, the default value is the name of the instance registered
         
        Default value is otherwise "*" which will search for all AOS instances
         
    .PARAMETER Aos
        Instruct the cmdlet to include the AOS service
         
    .PARAMETER ManagementReporter
        Instruct the cmdlet to include the ManagementReporter service
         
    .PARAMETER DIXF
        Instruct the cmdlet to include the DIXF service
         
    .PARAMETER ScanAllAosServices
        Instruct the cmdlet to look for all available AOS Instances on the computer
         
    .EXAMPLE
        PS C:\> Get-AxService
         
        This will get the status for all the default services from the local computer.
        If AxActiveAosConfiguration has been configured, it will work against the ComputerName and AosInstanceName registered.
         
    .EXAMPLE
        PS C:\> Get-AxService -ScanAllAosServices
         
        This will scan for all available AOS Services.
        If AxActiveAosConfiguration has been configured, it will work against the ComputerName registered otherwise localhost is used.
         
    .EXAMPLE
        PS C:\> Get-AxService -ComputerName TEST-AOS-01 -Aos
         
        This will get all AOS instances (services) from the server named "TEST-AOS-01".
        If AxActiveAosConfiguration has been configured, it will work against the AosInstanceName registered otherwise it will find all.
         
    .EXAMPLE
        PS C:\> Get-AxService -ComputerName TEST-AOS-01 -Aos -AosInstanceName *DEV*
         
        This will get all AOS instances (services) that match the search pattern "*DEV*" from the server named "TEST-AOS-01".
         
    .EXAMPLE
        PS C:\> Get-AxService -ComputerName TEST-AOS-01 -Aos | Start-AxService -ShowOriginalProgress
         
        This will scan the "TEST-AOS-01" server for all AOS instances (services) and start them.
        It will show the status for the service(s) on the server afterwards.
         
        If AxActiveAosConfiguration has been configured, it will work against the AosInstanceName registered otherwise it will find all.
         
    .EXAMPLE
        PS C:\> Get-AxService -ComputerName TEST-AOS-01 -Aos | Stop-AxService -ShowOriginalProgress
         
        This will scan the "TEST-AOS-01" server for all AOS instances (services) and stop them.
        It will show the status for the service(s) on the server afterwards.
         
        If AxActiveAosConfiguration has been configured, it will work against the AosInstanceName registered otherwise it will find all.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxService {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Alias('ServerName')]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string[]] $ComputerName = $Script:ActiveAosComputername,

        [Parameter(ParameterSetName = 'Default')]
        [switch] $All = $true,
        
        [Alias('InstanceName')]
        [string] $AosInstanceName = $(if (-not ([System.String]::IsNullOrEmpty($Script:ActiveAosInstancename))) { "*$Script:ActiveAosInstancename" } else { "*" }),

        [Parameter(ParameterSetName = 'Specific')]
        [switch] $Aos,

        [Parameter(ParameterSetName = 'Specific')]
        [switch] $ManagementReporter,

        [Parameter(ParameterSetName = 'Specific')]
        [switch] $DIXF,

        [switch] $ScanAllAosServices
    )

    begin {
        if ($PSCmdlet.ParameterSetName -eq "Specific") {
            $All = $false
        }
    
        if (!$All -and !$Aos -and !$ManagementReporter -and !$DIXF) {
            Write-PSFMessage -Level Host -Message "You have to use at least one switch when running this cmdlet. Please run the cmdlet again."
            Stop-PSFFunction -Message "Stopping because of missing parameters"
            return
        }

        $baseParams = Get-DeepClone $PSBoundParameters

        $params = @{ }
        $includeParamNames = @("ManagementReporter", "DIXF")

        foreach ($key in $baseParams.Keys) {
            if ($includeParamNames -notcontains $key ) { continue }

            Write-PSFMessage -Level Verbose -Message "Working on key: $key" -Target $key
        
            $null = $params.Add($key, $true)
        }

        if ($params.Count -eq 0) {
            if ($All) {
                $params.AllAxServices = $true

                $Services = @(Get-ServiceList @params)
            }
        }
        else {
            $Services = @(Get-ServiceList @params)
        }

        if ($PSBoundParameters.ContainsKey("Aos")) {
            Write-PSFMessage -Level Verbose -Message "Aos seems to be bound" -Target $key
        
            $searchServicesAos = @(Get-ServiceList -Aos)
        }

    }
    
    process {
        if (Test-PSFFunctionInterrupt) { return }
        
        $res = New-Object System.Collections.ArrayList
    
        foreach ($server in $ComputerName) {
            Write-PSFMessage -Level Verbose -Message "Working against: $server - listing services" -Target ($Services -Join ",")
            Write-PSFMessage -Level Verbose -Message "Working against: $server - listing Aos services" -Target ($searchServicesAos -Join ",")
        
            if ($null -ne $searchServicesAos -and $searchServicesAos.count -gt 0) {

                Write-PSFMessage -Level Verbose -Message "`$searchServicesAos used for searching"

                $colAosServices = Get-Service -ComputerName $server -Name $searchServicesAos -ErrorAction SilentlyContinue | Select-Object @{Name = "Server"; Expression = { $Server } }, Name, Status, DisplayName

                foreach ($service in $colAosServices) {
                    if ((-not $ScanAllAosServices) -and ($service.DisplayName -NotLike $AosInstanceName)) { continue }

                    $null = $res.Add($service)
                }
            }

            if ($null -ne $Services -and $Services.count -gt 0) {
            
                Write-PSFMessage -Level Verbose -Message "`$Services used for searching"

                $axServices = Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Select-Object @{Name = "Server"; Expression = { $Server } }, Name, Status, DisplayName
    
                foreach ($service in $axServices) {
                    if ($service.DisplayName -like "*AX Object Server*" ) {
                        if ((-not $ScanAllAosServices) -and ($service.DisplayName -NotLike $AosInstanceName)) { continue }
                    }

                    $null = $res.Add($service)
                }
            }
        }
        
        $res.ToArray() | Select-Object Server, DisplayName, Status, Name
    }
}


<#
    .SYNOPSIS
        Get the SSRS configuration
         
    .DESCRIPTION
        Get all the SSRS configuration from the AX 2012 database
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER DatabaseName
        Name of the database
         
        Default value is: "MicrosoftDynamicsAx"
         
    .PARAMETER SqlUser
        User name of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER SqlPwd
        Password of the SQL Server credential that you want to use when working against the database
         
    .EXAMPLE
        PS C:\> Get-AxSsrsConfig
         
        This will get all the stored SSRS configuration entries from the default DatabaseServer and the default Database.
         
    .EXAMPLE
        PS C:\> Get-AxAosInstance | Get-AxSsrsConfig
         
        This will get all AOS Instance from the local machine and pipe them to the Get-AxSsrsConfig cmdlet.
        The Get-AxSsrsConfig will the traverse every AOS Instance and their corresponding database for all SSRS configuration.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
#>

function Get-AxSsrsConfig {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 1)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)]
        [string] $DatabaseName = $Script:ActiveAosDatabase,

        [Parameter(Mandatory = $false, Position = 3)]
        [string] $SqlUser,

        [Parameter(Mandatory = $false, Position = 4)]
        [string] $SqlPwd
    )

    begin {
        Invoke-TimeSignal -Start
    }

    process {

        $baseParams = Get-DeepClone $PSBoundParameters
        $baseParams.Add("TrustedConnection", $true)

        $UseTrustedConnection = Test-TrustedConnection $baseParams

        $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
            SqlUser = $SqlUser; SqlPwd = $SqlPwd
        }

        $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection

        $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\get-ssrsconfig.sql") -join [Environment]::NewLine

        try {
            Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand)

            $sqlCommand.Connection.Open()

            $reader = $sqlCommand.ExecuteReader()

            while ($reader.Read() -eq $true) {
                [PSCustomObject][ordered]@{
                    ConfigurationId            = "$($reader.GetString($($reader.GetOrdinal("CONFIGURATIONID"))))"
                    Description                = "$($reader.GetString($($reader.GetOrdinal("DESCRIPTION"))))"
                    IsDefaultReportServer      = [bool][int]"$($reader.GetInt32($($reader.GetOrdinal("ISDEFAULTREPORTLIBRARYSERVER"))))"
                    SsrsServerName             = "$($reader.GetString($($reader.GetOrdinal("SERVERID"))))"
                    SsrsServerInstance         = "$($reader.GetString($($reader.GetOrdinal("SERVERINSTANCE"))))"
                    ReportManagerUrl           = "$($reader.GetString($($reader.GetOrdinal("REPORTMANAGERURL"))))"
                    ServerUrl                  = "$($reader.GetString($($reader.GetOrdinal("SERVERURL"))))"
                    IsSharepointIntegrated     = [bool][int]"$($reader.GetInt32($($reader.GetOrdinal("ISSHAREPOINTINTEGRATED"))))"
                    ReportFolder         = "$($reader.GetString($($reader.GetOrdinal("AXAPTAREPORTFOLDER"))))"
                    IsDefaultReportmodelServer = [bool][int]"$($reader.GetInt32($($reader.GetOrdinal("ISDEFAULTREPORTMODELSERVER"))))"
                    DataSourceName             = "$($reader.GetString($($reader.GetOrdinal("DATASOURCENAME"))))"
                    AosId                      = "$($reader.GetString($($reader.GetOrdinal("AOSID"))))"
                }
            }
        }
        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 {
            if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
                $sqlCommand.Connection.Close()
            }
    
            $sqlCommand.Dispose()
        }
    }
    end {
        Invoke-TimeSignal -End

    }
}


<#
 
InstanceName : MicrosoftDynamicsAX
ConfigurationName : DEBUG
BinDirectory : C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\Bin\
ExecutablePath : C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\Bin\Ax32Serv.exe
FileVersion : 6.3.6000.7046
ProductVersion : 6.3.6000.7046
FileVersionUpdated : 6.3.6000.7046
ProductVersionUpdated : 6.3.6000.7046
DatabaseServer : GJ-AX2012CU13
DatabaseName : MicrosoftDynamicsAX
ModelstoreDatabase : MicrosoftDynamicsAX_model
AosPort : 2712
WsdlPort : 8101
NetTcpPort : 8201
RegistryKeyPath : HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Dynamics Server\6.0\01
InstanceNumber : 01
ComputerName : GJ-AX2012CU13
 
 
SERVERID ISDEFAULTREPORTMODELSERVER SERVERURL ISDEFAULTREPORTLIBRARYSERVER AXAPTAREPORTFOLDER DESCRIPTION DATASOURCENAME REPORTMANAGERURL SERVERINSTANCE AOSID CONFIGURATIONID ISSHAREPOINTINTEGRATED RECVERSION RECID
ESADM01 0 http://esadm01:81/ReportServer 1 DynamicsAX http://esadm01:81/Reports MSSQLSERVER 01@ESADM01 SSRS_LIVE 0 546259976 5637145176
 
#>



<#
    .SYNOPSIS
        Get WMDP details
         
    .DESCRIPTION
        Get the most relevant WMDP details from your AX 2012 environment
         
    .EXAMPLE
        PS C:\> Get-AxWMDPDetails
         
        This will get all the relevant WMDP details from the AX 2012 environment
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxWMDPDetails {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    param ( )

    Get-WMDPDetailsFromIIS
}


<#
    .SYNOPSIS
        Import an AX 2012 modelstore file
         
    .DESCRIPTION
        Import an AX 2012 modelstore file into the modelstore database
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER ModelstoreDatabase
        Name of the modelstore database
         
        Default value is: "MicrosoftDynamicsAx_model"
         
        Note: From AX 2012 R2 and upwards you need to provide the full name for the modelstore database. E.g. "AX2012R3_PROD_model"
         
    .PARAMETER SchemaName
        Name of the schema to import the modelstore into
         
        Default value is: "TempSchema"
         
    .PARAMETER Path
        Path to the location where you want the file to be exported
         
        Default value is: "c:\temp\ax2012.tools"
         
    .PARAMETER IdConflictMode
        Parameter to instruct how the import should handle ID conflicts if it hits any during the import
         
        Valid options:
        "Reject"
        "Push"
        "Overwrite"
         
    .PARAMETER Apply
        Instruct the cmdlet to switch modelstore with the SchemaName in as the current code
         
    .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 generate the needed command and not execute it
         
    .EXAMPLE
        PS C:\> Import-AXModelStoreV2 -SchemaName TempSchema -Path C:\Temp\ax2012.tools\MicrosoftDynamicsAx.axmodelstore
         
        This will execute the cmdlet with some of the default values.
        This will work against the SQL server that is on localhost.
        The database is expected to be "MicrosoftDynamicsAx_model".
        The import will import the modelstore into the "TempSchema".
        The path where the modelstore file you want to import must exists is: "c:\temp\ax2012.tools\MicrosoftDynamicsAx.axmodelstore".
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Import-AXModelStoreV2 {
    [CmdletBinding(DefaultParameterSetName = "ImportModelstore")]
    [OutputType([System.String], ParameterSetName = "Generate")]
    Param(
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [string] $ModelstoreDatabase = $Script:ActiveAosModelstoredatabase,

        [Parameter(ParameterSetName = "ImportModelstore")]
        [Parameter(ParameterSetName = "ApplyModelstore")]
        [string] $SchemaName = "TempSchema",

        [Parameter(ParameterSetName = "ImportModelstore")]
        [string] $Path = (Join-Path $Script:DefaultTempPath "MicrosoftDynamicsAx.axmodelstore"),

        [ValidateSet("Reject", "Push", "Overwrite")]
        [Parameter(ParameterSetName = "ImportModelstore")]
        [string] $IdConflictMode,
        
        [Parameter(ParameterSetName = "ApplyModelstore")]
        [switch] $Apply,

        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start

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

    if (-not (Test-PathExists -Path $Script:AxPowerShellModule -Type Leaf)) { return }

    $params = @{
        Server   = $DatabaseServer
        Database = $ModelstoreDatabase
    }

    if ($PSCmdlet.ParameterSetName -eq "ImportModelstore") {
        $params.File = $Path
        $params.SchemaName = $SchemaName

        if ($PSBoundParameters.ContainsKey("IdConflictMode")) {
            $params.IdConflict = $IdConflictMode
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq "ApplyModelstore") {
        $params.Apply = $SchemaName
    }

    if ($OutputCommandOnly) {
        $arguments = Convert-HashToArgString -InputObject $params

        "Import-AxModelStore $($arguments -join ' ')"
    }
    else {
        Write-PSFMessage -Level Verbose -Message "Starting the export of the model store"
      
        $null = Import-Module $Script:AxPowerShellModule

        $outputRes = Import-AXModelStore @params

        if ($ShowOriginalProgress) {
            $outputRes
        }

        Clear-Ax2012StandardPowershellModule
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Import AX 2012 model
         
    .DESCRIPTION
        Import AX 2012 model into the AX 2012 Model store
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER ModelstoreDatabase
        Name of the modelstore database
         
        Default value is: "MicrosoftDynamicsAx_model"
         
        Note: From AX 2012 R2 and upwards you need to provide the full name for the modelstore database. E.g. "AX2012R3_PROD_model"
         
    .PARAMETER Path
        Path to the folder containing the AX model file(s) that you want to import
         
        The cmdlet will traverse all sub folders for files and import them based on their names
         
    .PARAMETER ConflictMode
        Instructs the cmdlet to handle conflicts
         
        The list of options is:
        "Reject"
        "Push"
        "Overwrite"
         
    .PARAMETER Detailed
        Instruct the cmdlet to output detailed element names and AOT path while importing the model
         
    .PARAMETER CreateParents
        Instruct the cmdlet to create missing parents on import
         
    .PARAMETER NoOptimize
        Instruct the cmdlet to skip the optimization on import
         
        This makes sense if you are import more than 1-2 AX 2012 models at the same time
         
    .PARAMETER NoPrompt
        Instruct the cmdlet not to prompt you with anything
         
    .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 output a script that you can execute manually later
         
        Using this will not import any AX 2012 models into the model store
         
    .EXAMPLE
        PS C:\> Import-AxModelV2 -Path "c:\temp\ax2012.tools\dev-models"
         
        The cmdlet will look for all the AX 2012 models located in "c:\temp\ax2012.tools\dev-models" or any of its sub folders.
        The ConflictMode is set to the default value of "OverWrite".
        The Database Server is set to the default value of "localhost".
        The Modelstore Database is set to the default value of "MicrosoftDynamicsAx_model".
         
         
    .EXAMPLE
        PS C:\> Import-AxModelV2 -Path "c:\temp\ax2012.tools\dev-models" -ShowOriginalProgress
         
        The cmdlet will look for all the AX 2012 models located in "c:\temp\ax2012.tools\dev-models" or any of its sub folders.
        The ConflictMode is set to the default value of "OverWrite".
        The Database Server is set to the default value of "localhost".
        The Modelstore Database is set to the default value of "MicrosoftDynamicsAx_model".
         
        It will show the original progress output for the import of the model file in real time.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Import-AxModelV2 {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName="Generate")]
    Param(
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $ModelstoreDatabase = $Script:ActiveAosModelstoredatabase,
        
        [Parameter(Mandatory = $false)]
        [string] $Path = $Script:DefaultTempPath,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Reject", "Push", "Overwrite")]
        [string] $ConflictMode = "Overwrite",
        
        [switch] $Detailed,

        [switch] $CreateParents,
        
        [switch] $NoOptimize,

        [switch] $NoPrompt,

        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly
    )

    BEGIN {
        $null = Import-Module $Script:AxPowerShellModule

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

    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }

        Invoke-TimeSignal -Start

        $AxModelsPath = (Get-ChildItem -Path $Path | Where-Object { $_.PSIsContainer } | Sort-Object CreationTime -Descending | Select-Object -First 1 | Select-Object Fullname).FullName
        Write-PSFMessage -Level Verbose -Message "The newest / latest folder is: $AxModelsPath" -Target $AxModelsPath

        $AxModelFiles = Get-ChildItem -Path $AxModelsPath -Recurse -File
        
        $params = @{ Server = $DatabaseServer; Conflict = $ConflictMode;
            Database = $ModelstoreDatabase
        }
        
        $paramsSwitch = Get-HashtableKey -InputObject $PSBoundParameters -Keys @("CreateParents", "NoOptimize", "NoPrompt")

        if($Detailed){
            $paramsSwitch.Add("Details", $Detailed)
        }
        
        foreach ($item in $AxModelFiles) {
            Write-PSFMessage -Level Verbose -Message "Working on file: $($item.FullName)" -Target $item.FullName
            $clonedParams = Get-DeepClone $params
            $clonedParams.File = $item.FullName
            
            if ($OutputCommandOnly) {
                $arguments = Convert-HashToArgString -InputObject $clonedParams
                $argumentsSwitch = Convert-HashToArgStringSwitch -InputObject $paramsSwitch
                "Install-AxModel $($arguments -join ' ') $($argumentsSwitch -join ' ')"
            }
            else {
                $outputRes = Install-AXModel @clonedParams @paramsSwitch

                if($ShowOriginalProgress) {
                    $outputRes
                }
            }
        }
        
        Invoke-TimeSignal -End
    }

    END {

        Clear-Ax2012StandardPowershellModule
    }
}


<#
    .SYNOPSIS
        Initialize an AX 2012 modelstore
         
    .DESCRIPTION
        Initialize an AX 2012 modelstore against a modelstore database
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER ModelstoreDatabase
        Name of the modelstore database
         
        Default value is: "MicrosoftDynamicsAx_model"
         
        Note: From AX 2012 R2 and upwards you need to provide the full name for the modelstore database. E.g. "AX2012R3_PROD_model"
         
    .PARAMETER SchemaName
        Name of the schema in the modelstore database that you want to work against
         
        Default value is: "TempSchema"
         
    .PARAMETER DropSchema
        Instruct the cmdlet to drop the schema supplied with the -SchemaName parameter
         
    .PARAMETER CreateSchema
        Instruct the cmdlet to create the schema supplied with the -SchemaName parameter
         
    .PARAMETER CreateDb
        Instruct the cmdlet to create a new modelstore inside the supplied -ModelstoreDatabase parameter
         
    .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 generate the needed command and not execute it
         
    .EXAMPLE
        PS C:\> Initialize-AXModelStoreV2 -SchemaName TempSchema -CreateSchema
         
        This will execute the cmdlet with some of the default values.
        This will work against the SQL server that is on localhost.
        The database is expected to be "MicrosoftDynamicsAx_model".
        The cmdlet will create the "TempSchema" schema inside the modelstore database.
         
    .EXAMPLE
        PS C:\> Initialize-AXModelStoreV2 -SchemaName TempSchema -DropSchema
         
        This will execute the cmdlet with some of the default values.
        This will work against the SQL server that is on localhost.
        The database is expected to be "MicrosoftDynamicsAx_model".
        The cmdlet will drop the "TempSchema" schema inside the modelstore database.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Initialize-AXModelStoreV2 {
    [CmdletBinding(DefaultParameterSetName = "CreateSchema")]
    [OutputType([System.String], ParameterSetName = "Generate")]
    param (
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [string] $ModelstoreDatabase = $Script:ActiveAosModelstoredatabase,

        [Parameter(ParameterSetName = "Drop")]
        [Parameter(ParameterSetName = "CreateSchema")]
        [string] $SchemaName = "TempSchema",

        [Parameter(ParameterSetName = "Drop")]
        [switch] $DropSchema,

        [Parameter(ParameterSetName = "CreateSchema")]
        [switch] $CreateSchema,

        [Parameter(ParameterSetName = "CreateDB")]
        [switch] $CreateDb,
        
        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start
    
    if (-not (Test-PathExists -Path $Script:AxPowerShellModule -Type Leaf)) { return }

    $params = @{
        Server   = $DatabaseServer
        Database = $ModelstoreDatabase
    }

    if ($PSCmdlet.ParameterSetName -eq "CreateSchema") {
        $params.SchemaName = $SchemaName
    }
    elseif ($PSCmdlet.ParameterSetName -eq "Drop") {
        $params.Drop = $SchemaName
    }
    elseif ($PSCmdlet.ParameterSetName -eq "CreateDB") {
        $params.CreateDB = $true
    }

    if ($OutputCommandOnly) {
        $arguments = Convert-HashToArgString -InputObject $params

        "Initialize-AXModelStore $($arguments -join ' ')"
    }
    else {
        Write-PSFMessage -Level Verbose -Message "Starting the initialization of the model store"
        
        $null = Import-Module $Script:AxPowerShellModule
        
        $outputRes = Initialize-AXModelStore @params

        if ($ShowOriginalProgress) {
            $outputRes
        }
                
        Clear-Ax2012StandardPowershellModule
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Start the AxBuild.exe
         
    .DESCRIPTION
        Invoke the AxBuild.exe with the necessary parameters to make it compile your application
         
    .PARAMETER BinDirectory
        The full path to the bin directory where the AOS instance is physical installed
         
    .PARAMETER AlternativeBinPath
        The full path to the client bin directory where AX 2012 Client is physical installed
         
    .PARAMETER InstanceNumber
        The 2 digit ([0-9][0-9]) number that the AOS instance has on the server
         
    .PARAMETER DatabaseServer
        The name of the server running SQL Server
         
    .PARAMETER ModelstoreDatabase
        The name of the AX 2012 modelstore database
         
    .PARAMETER Workers
        Number of workers that you want to utilize while compiling
         
        The built-in logic from AxBuild.exe will choose a number equal to your visible cores
        Leaving it blank or with 0 (Zero) will use the built-in logic from AxBuild.exe
         
    .PARAMETER OutputPath
        Path to the log file you want AxBuild.exe to output to
         
        Default location is: "c:\temp\ax2012.tools\AxBuild\"
         
    .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 output a script that you can execute manually later
         
        Using this will not import any AX 2012 models into the model store
         
    .EXAMPLE
        PS C:\> Get-AxAosInstance | Invoke-AxBuild
         
        This will find all AOS instances using the Get-AxAosInstance on the machine and pipe them to Invoke-AxBuild.
        For each AOS instance found it will start the AxBuild.exe against their individual details.
        It will store the log file under the default ax2012.tools folder.
         
    .EXAMPLE
        PS C:\> Invoke-AxBuild
         
        This will start the AxBuild.exe against the ActiveAos configuration.
        It will store the log file under the default ax2012.tools folder.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
#>

function Invoke-AxBuild {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName = "Generate")]
    param (

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Path')]
        [string] $BinDirectory = $Script:ActiveAosBindirectory,

        [Alias('AltBin')]
        [string] $AlternativeBinPath = $Script:ClientBin,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Aos')]
        [string] $InstanceNumber = $Script:ActiveAosInstanceNumber,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('DBServer')]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Modelstore')]
        [string] $ModelstoreDatabase = $Script:ActiveAosModelstoredatabase,

        [int] $Workers,

        [string] $OutputPath = $(Join-Path $Script:DefaultTempPath "AxBuildLog"),

        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly
    )

    begin {
        $executable = Join-Path $BinDirectory "AXBuild.exe"
        $compiler = Join-Path $BinDirectory "ax32serv.exe"

        if (-not (Test-PathExists -Path $OutputPath -Type Container -Create)) { return }
        if (-not (Test-PathExists -Path $executable, $compiler -Type Leaf)) { return }
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }

        Invoke-TimeSignal -Start

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

        $params.Add("xppcompileall")
        $params.Add("/altbin=`"$AlternativeBinPath`"")
        $params.Add("/aos=$InstanceNumber")
        $params.Add("/dbserver=`"$DatabaseServer`"")
        $params.Add("/modelstore=`"$ModelstoreDatabase`"")
        $params.Add("/log=`"$OutputPath`"")
        $params.Add("/compiler=`"$compiler`"")

        if ((-not ($null -eq $Workers)) -and ($Workers -gt 0)) {
            $params.Add("/workers=$Workers")
        }

        Write-PSFMessage -Level Verbose -Message "Starting $executable with $($params -join " ")" -Target ($params -join " ")

        Invoke-Process -Executable $executable -Params $($params.ToArray()) -ShowOriginalProgress:$ShowOriginalProgress -OutputCommandOnly:$OutputCommandOnly

        Invoke-TimeSignal -End

    }
}


<#
    .SYNOPSIS
        Invoke a http request for a Logic App
         
    .DESCRIPTION
        Invoke a Logic App using a http request and pass a json object with details about the calling function
         
    .PARAMETER Url
        The URL for the http endpoint that you want to invoke
         
    .PARAMETER Email
        The email address of the receiver of the message that the cmdlet will send
         
    .PARAMETER Subject
        Subject string to apply to the email and to the IM message
         
    .PARAMETER IncludeAll
        Instruct the cmdlet to include all cmdlets (names only) from the pipeline
         
    .PARAMETER AsJob
        Instruct the cmdlet to run the invocation as a job (async)
         
    .EXAMPLE
        PS C:\> Invoke-AxLogicApp -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/
         
        This will simply invoke an Azure Logic App with the email, subject and URL parameters that are needed to invoke an Azure Logic App.
        A notification will be sent to "administrator@contoso.com".
         
    .EXAMPLE
        PS C:\> Start-AxEnvironment -Aos | Invoke-AxLogicApp
         
        This will execute the sync process and when it is done it will invoke an Azure Logic App with the default parameters that have been configured for the system.
         
    .EXAMPLE
        PS C:\> Start-AxEnvironment -Aos | Invoke-AxLogicApp -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/
         
        This will execute the sync process and when it is done it will invoke an Azure Logic App with the email, subject and URL parameters that are needed to invoke an Azure Logic App.
        A notification will be sent to "administrator@contoso.com".
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Invoke-AxLogicApp {
    param (
        [string] $Url = $Script:ActiveLogicappUrl,
        
        [string] $Email = $Script:ActiveLogicappEmail,

        [string] $Subject = $Script:ActiveLogicappSubject,

        [switch] $IncludeAll,
        
        [switch] $AsJob

    )

    begin {
    }
    
    process {
        $pipes = $MyInvocation.Line.Split("|")
         
        $arrList = New-Object -TypeName "System.Collections.ArrayList"
        foreach ($item in $pipes.Trim()) {
            $null = $arrList.Add( $item.Split(" ")[0])
        }

        $strMessage = "";

        if ($IncludeAll) {
            $strMessage = $arrList -Join ", "
        }
        else {
            $strMessage = $arrList[$MyInvocation.PipelinePosition - 2]
        }

        $strMessage = "The following list of cmdlets has executed: $strMessage"
        
        Invoke-PSNMessage -Url $URL -ReceiverEmail $Email -Subject $Subject -Message $strMessage -AsJob:$AsJob
    }
    
    end {
    }
}


<#
    .SYNOPSIS
        Execute the AutoRun feature of the AX 2012 Client
         
    .DESCRIPTION
        AX 2012 offers several ways to automate some core developer tasks, like DB Sync & Xpp Compile
         
    .PARAMETER Path
        Path to the autorun xml file that contains the desired automation tasks that you want to execute
         
    .PARAMETER AxClientExePath
        Path to the AX 2012 Client (ax32.exe) file, which is needed to run the automated tasks
         
        The default value is read from the registry on the local machine
         
    .PARAMETER TimeoutInMinutes
        Number of minutes the autorun task is allowed to run, before you want it to exit
         
        Default value is: 360 minutes (6 hours)
         
    .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 generate the needed command and not execute it
         
    .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-AxStartAutoRun -Path "c:\temp\ax2012.tools\autorun_syncDB.xml"
         
        This will invoke the autorun feature of the AX 2012 client.
        It will use the "c:\temp\ax2012.tools\autorun_syncDB.xml" as path passed to the AX 2012 Client.
        It will use the default path of the AX 2012 Client, read from the registry.
        It will run for a maximum of 360 minutes.
         
    .NOTES
        Tags:
         
        Author: M�tz Jensen (@Splaxi)
         
#>


function Invoke-AxStartAutoRun {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName = "Generate")]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('AutoRunXml')]
        [string] $Path,

        [string] $AxClientExePath = $(Join-Path $Script:ClientBin "Ax32.exe"),

        [Int32] $TimeoutInMinutes = 360,

        [switch] $ShowOriginalProgress,

        [Parameter(ParameterSetName = "Generate")]
        [switch] $OutputCommandOnly,

        [switch] $EnableException
    )

    $executable = $AxClientExePath
    $logPath = ""

    if (-not (Test-PathExists -Path $Path, $executable, $AxClientExePath -Type Leaf)) { return }

    if (Test-PSFFunctionInterrupt) { return }

    $autoRunNode = Select-Xml -Path $Path -XPath "//AxaptaAutoRun"
    
    if ([System.IO.Path]::HasExtension($($autoRunNode.Node.logFile))) {
        $logPath = $autoRunNode.Node.logFile
        $parentPath = Split-Path -Path $logPath -Parent

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

        if (Test-PSFFunctionInterrupt) { return }
    }

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

    $params.Add("-development")
    $params.Add("-internal=noModalBoxes")
    $params.Add("-StartupCmd=autorun_`"$Path`"")

    Invoke-Process -Executable $executable -Params $($params.ToArray()) -TimeoutInMinutes $TimeoutInMinutes -ShowOriginalProgress:$ShowOriginalProgress -OutputCommandOnly:$OutputCommandOnly -EnableException:$EnableException

    if (-not ($OutputCommandOnly)) {
        # [string]$logString = Get-Content -Path $logPath -Raw

        # [xml]$outputContent = $logString.Replace("&", "&amp;")

        # $stringWriter = New-Object System.IO.StringWriter

        # $xmlSettings = New-object System.Xml.XmlWriterSettings
        # $xmlSettings.Encoding = [System.Text.Encoding]::UTF8
        # $xmlSettings.Indent = $true
        # $xmlSettings.NewLineHandling = [System.Xml.NewLineHandling]::Entitize
        
        # $xmlWriter = [System.Xml.XmlWriter]::Create($stringWriter, $xmlSettings)
        
        # # $xmlWriter.Indentation = 4
        
        # $outputContent.WriteTo($xmlWriter)
        # $xmlWriter.Flush()
        # $stringWriter.Flush()
        # $outputContent.LoadXml($stringWriter.ToString())
        # $outputContent.Save($logPath)

        [PSCustomObject]@{
            Path = $logPath
        }
    }
}


<#
    .SYNOPSIS
        Remove environment configuration
         
    .DESCRIPTION
        Remove a environment configuration from the configuration store
         
    .PARAMETER Name
        Name of the environment configuration you want to remove from the configuration store
         
    .PARAMETER Temporary
        Instruct the cmdlet to only temporarily remove the environment configuration from the configuration store
         
    .EXAMPLE
        PS C:\> Remove-AxEnvironmentConfig -Name "UAT"
         
        This will remove the environment configuration name "UAT" from the machine.
         
    .NOTES
        Tags: Servicing, Environment, Config, Configuration, Servers
         
        Author: M�tz Jensen (@Splaxi)
         
    .LINK
        Add-AxEnvironmentConfig
         
    .LINK
        Clear-AxActiveEnvironmentConfig
         
    .LINK
        Get-AxActiveEnvironmentConfig
         
    .LINK
        Get-AxEnvironmentConfig
         
    .LINK
        Set-AxActiveEnvironmentConfig
         
#>


function Remove-AxEnvironmentConfig {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    [OutputType()]
    param (
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $Name,

        [switch] $Temporary
    )

    $Name = $Name.ToLower()

    if ($Name -match '\*') {
        Write-PSFMessage -Level Host -Message "The name cannot contain <c='em'>wildcard character</c>."
        Stop-PSFFunction -Message "Stopping because the name contains wildcard character."
        return
    }

    if (-not ((Get-PSFConfig -FullName "ax2012.tools.environment.*.name").Value -contains $Name)) {
        Write-PSFMessage -Level Host -Message "A environment configuration with that name <c='em'>doesn't exists</c>."
        Stop-PSFFunction -Message "Stopping because a environment message configuration with that name doesn't exists."
        return
    }

    $res = (Get-PSFConfig -FullName "ax2012.tools.environment.config.name").Value

    if ($res -eq $Name) {
        Write-PSFMessage -Level Host -Message "The active environment configuration is the <c='em'>same as the one you're trying to remove</c>. Please set another configuration as active, before removing this one. You could also call Clear-AxActiveEnvironmentConfig."
        Stop-PSFFunction -Message "Stopping because the active environment configuration is the same as the one trying to be removed."
        return
    }

    foreach ($config in Get-PSFConfig -FullName "ax2012.tools.environment.$Name.*") {
        Set-PSFConfig -FullName $config.FullName -Value ""

        if (-not $Temporary) { Unregister-PSFConfig -FullName $config.FullName -Scope UserDefault }
    }
}


<#
    .SYNOPSIS
        Fix table and field ID conflicts
         
    .DESCRIPTION
        Fixes both table and field IDs in the AX SqlDictionary (data db) to match the AX code (Model db)
         
        Useful for after a database has been restored and the table or field IDs do not match
        Run this command instead of letting the database synchronization process drop and recreate the table
         
        Before running:
        Stop the AOS
        Always take the appropriate SQL backups before running this script
         
        After running:
        Start the AOS
        Sync the database within AX
         
        Note:
        Objects that are new in AOT will get created in SQL dictionary when synchronization happens
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER DatabaseName
        Name of the database
         
        Default value is: "MicrosoftDynamicsAx"
         
    .PARAMETER ModelstoreDatabase
        Name of the modelstore database
         
        Default value is: "MicrosoftDynamicsAx_model"
         
    .PARAMETER SqlUser
        User name of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER SqlPwd
        Password of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER Force
        Instruct the cmdlet to overwrite any existing bak (backup) tables from previous executions
         
    .PARAMETER OutputCommandOnly
        When provided the SQL is returned and not executed
         
        Note: This is useful for troubleshooting or providing the script to a DBA with access to the server
         
    .EXAMPLE
        PS C:\> Resolve-AxTableFieldIDs
         
        This will execute the cmdlet with all the default values.
        This will work against the SQL server that is on localhost.
        The database is expected to be "MicrosoftDynamicsAx_model".
         
    .NOTES
        Author: Dag Calafell, III (@dodiggitydag)
        Reference: http://calafell.me/the-ultimate-ax-2012-table-and-field-id-fix-for-synchronization-errors/
#>

Function Resolve-AxTableFieldIDs {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName = "Generate")]
    Param(
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [string] $DatabaseName = $Script:ActiveAosDatabase,

        [string] $ModelstoreDatabase = $Script:ActiveAosModelstoredatabase,

        [string] $SqlUser,

        [string] $SqlPwd,

        [Switch] $Force,

        [Parameter(ParameterSetName = "Generate")]
        [Switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start

    $baseParams = Get-DeepClone $PSBoundParameters
    $baseParams.Add("TrustedConnection", $true)

    $UseTrustedConnection = Test-TrustedConnection $baseParams

    $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd
    }

    $forceParameterValue = "0"

    if ($Force) { $forceParameterValue = "1" }

    $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection

    $commandText = (Get-Content "$script:ModuleRoot\internal\sql\resolve-sqldictionaryids.sql") -join [Environment]::NewLine
    
    $sqlCommand.CommandText = $commandText.Replace('@DatabaseName', $DatabaseName).Replace('@ModelDatabaseName', $ModelstoreDatabase).Replace("@ForceValue", $forceParameterValue)

    if ($OutputCommandOnly) {
        (Get-SqlString $sqlCommand)
    }
    else {
        $handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] { param($sender, $event) Write-PSFMessage -Level Host -Message $($event.Message) -Target $($event.Message) }
        $sqlCommand.Connection.add_InfoMessage($handler)
        $sqlCommand.Connection.FireInfoMessageEventOnUserErrors = $true;

        try {
            Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand)
            
            $sqlCommand.Connection.Open()
            $sqlCommand.ExecuteNonQuery()

            Write-PSFMessage -Level Verbose -Message "SQL query executed."
        }
        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 {
            if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
                $sqlCommand.Connection.Close()
            }
    
            $sqlCommand.Dispose()
        }
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Set the active AX 2012 AOS configuration
         
    .DESCRIPTION
        Set the active AX 2012 AOS details and store it into the configuration storage
         
    .PARAMETER ComputerName
        The name of the computer / server that AOS resides on
         
    .PARAMETER BinDirectory
        The full path to the bin directory where the AOS instance is physical installed
         
    .PARAMETER InstanceNumber
        The 2 digit ([0-9][0-9]) number that the AOS instance has on the server
         
    .PARAMETER InstanceName
        The instance name the AOS server is registered with
         
    .PARAMETER DatabaseServer
        The name of the server running SQL Server
         
    .PARAMETER DatabaseName
        The name of the AX 2012 business data database
         
    .PARAMETER ModelstoreDatabase
        The name of the AX 2012 modelstore database
         
    .PARAMETER AosPort
        The TCP port that the AX 2012 AOS server is communicating with the AX clients on
         
    .PARAMETER WsdlPort
        The TCP port that the AX 2012 AOS server is communicating with all WSDL consuming applications on
         
    .PARAMETER NetTcpPort
        The TCP port that the AX 2012 AOS server is communicating with all NetTcp consuming applications on
         
    .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
         
    .PARAMETER Clear
        Instruct the cmdlet to clear out all the stored configuration values
         
    .EXAMPLE
        PS C:\> Get-AxAosInstance | Select-Object -First 1 | Set-AxActiveAosConfig
         
        This will get all the AX 2012 AOS instances from the local machine and only select the first output.
        The output from the first AOS instance is saved into the configuration store.
         
    .EXAMPLE
        PS C:\> Set-AxActiveAosConfig -ComputerName AX2012PROD -DatabaseServer SQLSERVER -DatabaseName AX2012R3_PROD -ModelstoreDatabase AX2012R3_PROD_model -AosPort 2712
         
        This will update the active AOS configuration store settings.
        The computer name will be registered to: AX2012PROD
        The database server will be registered to: SQLSERVER
        The database name will be registered to: AX2012R3_PROD
        The model store database will be registered to: AX2012R3_PROD_model
        The AOS port will be registered to: 2712
         
    .EXAMPLE
        PS C:\> Set-AxActiveAosConfig -Clear
         
        This will clear out all the stored configuration values.
        It updates all the internal configuration variables, so all aos default values across the module will be empty.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Set-AxActiveAosConfig {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (

        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 1)]
        [string[]] $ComputerName = @($env:computername),

        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 2)]
        [string] $BinDirectory,

        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 3)]
        [string] $InstanceNumber,
        
        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 4)]
        [string] $InstanceName,
        
        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 5)]
        [string] $DatabaseServer,
                
        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 6)]
        [string] $DatabaseName,
        
        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 7)]
        [string] $ModelstoreDatabase,

        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 8)]
        [string] $AosPort,

        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 9)]
        [string] $WsdlPort,

        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 10)]
        [string] $NetTcpPort,
        
        [ValidateSet('User', 'System')]
        [string] $ConfigStorageLocation = "User",

        [switch] $Temporary,

        [switch] $Clear
    )

    $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation

    if (Test-PSFFunctionInterrupt) { return }
    
    if ($Clear) {

        Write-PSFMessage -Level Verbose -Message "Clearing all the ax2012.tools.active.aos configurations."

        foreach ($item in (Get-PSFConfig -FullName ax2012.tools.active.aos*)) {
            Set-PSFConfig -Fullname $item.FullName -Value ""
            if (-not $Temporary) { Register-PSFConfig -FullName $item.FullName -Scope $configScope }
        }
    }
    else {
        foreach ($key in $PSBoundParameters.Keys) {
            $value = $PSBoundParameters.Item($key)

            Write-PSFMessage -Level Verbose -Message "Working on $key with $value" -Target $value

            switch ($key) {
                "ComputerName" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.computername' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.computername' -Scope $configScope }
                }

                "BinDirectory" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.bindirectory' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.bindirectory' -Scope $configScope }
                }

                "InstanceNumber" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.instance.number' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.instance.number' -Scope $configScope }
                }

                "InstanceName" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.instancename' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.instancename' -Scope $configScope }
                }

                "DatabaseServer" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.databaseserver' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.databaseserver' -Scope $configScope }
                }
        
                "DatabaseName" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.database' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.database' -Scope $configScope }
                }

                "ModelstoreDatabase" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.modelstoredatabase' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.modelstoredatabase' -Scope $configScope }
                }

                "AosPort" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.aos.port' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.aos.port' -Scope $configScope }
                }

                "WsdlPort" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.wsdl.port' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.wsdl.port' -Scope $configScope }
                }

                "NetTcpPort" {
                    Set-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.nettcp.port' -Value $value
                    if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name 'active.aos.nettcp.port' -Scope $configScope }
                }

                Default {}
            }
       
        }
    }

    Update-ActiveVariables
}


<#
         
    .SYNOPSIS
        Set the active environment configuration
         
    .DESCRIPTION
        Updates the current active environment configuration with a new one
         
        Use this to update the default parameters across the module, to make it easier to call your different commands
         
    .PARAMETER Name
        Name of the environment configuration you want to load into the active environment configuration
         
    .PARAMETER Temporary
        Instruct the cmdlet to only temporarily override the persisted settings in the configuration store
         
    .EXAMPLE
        PS C:\> Set-AxActiveEnvironmentConfig -Name "UAT"
         
        This will set the environment configuration named "UAT" as the active configuration.
         
    .NOTES
        Tags: Servicing, Environment, Config, Configuration, Servers
         
        Author: M�tz Jensen (@Splaxi)
         
    .LINK
        Add-AxEnvironmentConfig
         
    .LINK
        Get-AxActiveEnvironmentConfig
         
    .LINK
        Get-AxEnvironmentConfig
         
#>


function Set-AxActiveEnvironmentConfig {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    [OutputType()]
    param (
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $Name,

        [switch] $Temporary
    )

    if($Name -match '\*') {
        $messageString = "The name cannot contain <c='em'>wildcard character</c>."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because the name contains wildcard character." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>','')))
        return
    }
    
    if (-not ((Get-PSFConfig -FullName "ax2012.tools.environment.*.name").Value -contains $Name)) {
        $messageString = "An environment configuration with that name <c='em'>doesn't exists</c>."
        Write-PSFMessage -Level Host -Message $messageString
        Stop-PSFFunction -Message "Stopping because an OData message configuration with that name doesn't exists." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>','')))
        return
    }
    
    Set-PSFConfig -FullName "ax2012.tools.active.environment.config.name" -Value $Name
    if (-not $Temporary) { Register-PSFConfig -FullName "ax2012.tools.active.environment.config.name"  -Scope UserDefault }

    Update-ActiveEnvironmentVariables
}


<#
    .SYNOPSIS
        Set the admin account inside the AX 2012 database
         
    .DESCRIPTION
        Set the user account details (credentails) that will be the considered as the admin account in the AX 2012 database
         
    .PARAMETER Username
        Username of the user that you want to be the new admin in the database
         
        Must include domain details, either in PRE-2000 or UPN style
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER DatabaseName
        Name of the database
         
        Default value is: "MicrosoftDynamicsAx"
         
    .PARAMETER SqlUser
        User name of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER SqlPwd
        Password of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER OutputCommandOnly
        Instruct the cmdlet to only generate the needed command and not execute it
         
    .EXAMPLE
        PS C:\> Set-AxAdmin -Username "ACME.local\test"
         
        This will update the admin record in the AX 2012 database to "ACME.local\test".
         
    .NOTES
        Tags:
         
        Author: M�tz Jensen (@Splaxi)
#>


function Set-AxAdmin {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName = "Generate")]
    param (
        
        [Parameter(Mandatory = $true)]
        [string] $Username,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseName = $Script:ActiveAosDatabase,

        [string] $SqlUser,

        [string] $SqlPwd,

        [Parameter(ParameterSetName = "Generate")]
        [Switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start

    $userInfo = Get-DomainUserDetails -Username $Username

    $baseParams = Get-DeepClone $PSBoundParameters
    $baseParams.Add("TrustedConnection", $true)

    $UseTrustedConnection = Test-TrustedConnection $baseParams

    $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd
    }

    $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\set-axadmin.sql") -join [Environment]::NewLine
    $null = $sqlCommand.Parameters.AddWithValue('@NetworkDomain', $userInfo.Domain)
    $null = $sqlCommand.Parameters.AddWithValue('@NetworkAlias', $userInfo.UserId)
    $null = $sqlCommand.Parameters.AddWithValue('@SID', $userInfo.Sid)

    # UPDATE [dbo].[USERINFO] SET [NETWORKDOMAIN] = @NetworkDomain, [NETWORKALIAS] = @NetworkAlias, [SID] = @SID WHERE [ID] = 'Admin'

    if ($OutputCommandOnly) {
        (Get-SqlString $sqlCommand)
    }
    else {
        try {
            Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand)

            $sqlCommand.Connection.Open()
            $sqlCommand.ExecuteNonQuery()

            Write-PSFMessage -Level Verbose -Message "SQL query executed."
        }
        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 {
            if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
                $sqlCommand.Connection.Close()
            }
    
            $sqlCommand.Dispose()
        }
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Set the details for the logic app invoke cmdlet
         
    .DESCRIPTION
        Store the needed details for the module to execute an Azure Logic App using a HTTP request
         
    .PARAMETER Url
        The URL for the http request endpoint of the desired logic app
         
    .PARAMETER Email
        The receiving email address that should be notified
         
    .PARAMETER Subject
        The subject of the email that you want to send
         
    .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-AxLogicAppConfig -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/
         
        This will set all the details about invoking the Logic App.
         
    .EXAMPLE
        PS C:\> Set-AxLogicAppConfig -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/ -ConfigStorageLocation "System"
         
        This will set all the details about invoking the Logic App.
        The data will be stored in the system wide configuration storage, which makes it accessible from all users.
         
    .EXAMPLE
        PS C:\> Set-AxLogicAppConfig -Email administrator@contoso.com -Subject "Work is done" -Url https://prod-35.westeurope.logic.azure.com:443/ -Temporary
         
        This will set all the details about invoking the Logic App.
        The update will only last for the rest of this PowerShell console session.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Set-AxLogicAppConfig {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true )]
        [string] $Url,

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

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

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

        [switch] $Temporary
    )

    $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation

    if (Test-PSFFunctionInterrupt) { return }
    
    foreach ($key in $PSBoundParameters.Keys) {
        $value = $PSBoundParameters.Item($key)
        $name = $null

        Write-PSFMessage -Level Verbose -Message "Working on $key with $value" -Target $value

        
        switch ($key) {
            "Email" {
                $name = 'active.logicapp.email'
                $Script:LogicAppEmail = $value
            }

            "Url" {
                $name = 'active.logicapp.url'
                $Script:LogicAppUrl = $value
            }

            "Subject" {
                $name = 'active.logicapp.subject'
                $Script:LogicAppSubject = $value
            }

            Default {}
        }

        Set-PSFConfig -Module 'ax2012.tools' -Name $name -Value $value
        if (-not $Temporary) { Register-PSFConfig -Module 'ax2012.tools' -Name $name -Scope $configScope }
    }
}


<#
    .SYNOPSIS
        Set the parameter sniffing configuration
         
    .DESCRIPTION
        Set the parameter sniffing value in the database based on the released hotfix from Microsoft for AX 2012
         
    .PARAMETER DatabaseServer
        Server name of the database server
         
        Default value is: "localhost"
         
    .PARAMETER DatabaseName
        Name of the database
         
        Default value is: "MicrosoftDynamicsAx"
         
    .PARAMETER SqlUser
        User name of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER SqlPwd
        Password of the SQL Server credential that you want to use when working against the database
         
    .PARAMETER OutputCommandOnly
        When provided the SQL is returned and not executed
         
        Note: This is useful for troubleshooting or providing the script to a DBA with access to the server
         
    .EXAMPLE
        PS C:\> Set-AxParameterSniffingSetting
         
        This will configure the correct parameter sniffing settings.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
    .LINK
        https://community.dynamics.com/365/financeandoperations/b/axsupport/posts/how-to-proactively-avoid-parameter-sniffing-step-by-step
#>

function Set-AxParameterSniffingSetting {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName="Generate")]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseServer = $Script:ActiveAosDatabaseserver,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string] $DatabaseName = $Script:ActiveAosDatabase,

        [string] $SqlUser,

        [string] $SqlPwd,

        [Parameter(ParameterSetName = "Generate")]
        [Switch] $OutputCommandOnly
    )

    Invoke-TimeSignal -Start

    $baseParams = Get-DeepClone $PSBoundParameters
    $baseParams.Add("TrustedConnection", $true)

    $UseTrustedConnection = Test-TrustedConnection $baseParams

    $SqlParams = @{ DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd
    }

    $sqlCommand = Get-SqlCommand @SqlParams -TrustedConnection $UseTrustedConnection

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\set-axparametersniffingsetting.sql") -join [Environment]::NewLine

    if ($OutputCommandOnly) {
        (Get-SqlString $sqlCommand)
    }
    else {
        try {
            Write-PSFMessage -Level InternalComment -Message "Executing a script against the database." -Target (Get-SqlString $sqlCommand)

            $sqlCommand.Connection.Open()
            $null = $sqlCommand.ExecuteNonQuery()

            Write-PSFMessage -Level Verbose -Message "SQL query executed."
        }
        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 {
            if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
                $sqlCommand.Connection.Close()
            }
    
            $sqlCommand.Dispose()
        }
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Start AX 2012 service(s)
         
    .DESCRIPTION
        Start AX 2012 service(s) on the computer
         
    .PARAMETER Server
        Name of the computer(s) that you want to work against
         
        Default value is the name from the ComputerName from AxActiveAosConfiguration
         
    .PARAMETER DisplayName
        DisplayName of windows service that you want to work against
         
        Accepts wildcards for searching. E.g. -DisplayName "*ax*obj*"
         
    .PARAMETER Name
        Name of the Windows Service that you want to work against
         
        This parameter is used when piping in the details
         
        Designed to work together with the Get-AxEnvironment cmdlet
         
    .PARAMETER ShowOriginalProgress
        Instruct the cmdlet to output the status for the service
         
    .EXAMPLE
        PS C:\> Start-AxService -Server TEST-AOS-01 -DisplayName *ax*obj*
         
        This will start the service(s) that match the search pattern "*ax*obj*" on the server named "TEST-AOS-01".
         
    .EXAMPLE
        PS C:\> Start-AxService -Server TEST-AOS-01 -DisplayName *ax*obj* -ShowOriginalProgress
         
        This will start the service(s) that match the search pattern "*ax*obj*" on the server named "TEST-AOS-01".
        It will show the progress of starting the service(s) in the console.
        It will show the status for the service(s) on the server afterwards.
         
    .EXAMPLE
        PS C:\> Get-AxService -ComputerName TEST-AOS-01 -Aos | Start-AxService -ShowOriginalProgress
         
        This will scan the "TEST-AOS-01" server for all AOS instances (services) and start them.
        It will show the progress of starting the service(s) in the console.
        It will show the status for the service(s) on the server afterwards.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Start-AxService {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = "Default")]
    param (

        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = "Pipeline")]
        [Parameter(ParameterSetName = "Default", Position = 1)]
        [Alias('ComputerName')]
        [string[]] $Server = $Script:ActiveAosComputername,

        [Parameter(Mandatory = $true, ParameterSetName = "Default", Position = 2)]
        [string] $DisplayName,

        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = "Pipeline")]
        [string[]] $Name,

        [switch] $ShowOriginalProgress
    )

    begin {
        $output = New-Object System.Collections.ArrayList

        $warningActionValue = "SilentlyContinue"
        if ($ShowOriginalProgress) { $warningActionValue = "Continue" }
    }

    process {
        $baseParams = @{ComputerName = $Server; ErrorAction = "SilentlyContinue" }

        if ($PSBoundParameters.ContainsKey("Name")) {
            foreach ($item in $Name) {
                if (($item.Trim().Length -eq 0) -or ($Name -eq "*")) {
                    Write-PSFMessage -Level Host -Message "It seems that you didn't provide any Name. That would result in starting all services." -Exception $PSItem.Exception
                    Stop-PSFFunction -Message "Stopping because of missing filters."
                    return
                }
            }

            if (Test-PSFFunctionInterrupt) { return }

            $baseParams.Name = $Name
        }
        else {
            if (($DisplayName.Length -gt 0) -and (-not($DisplayName -eq "*"))) {
                if ($DisplayName -notmatch "\*" ) {
                    $DisplayName = "*$DisplayName*"
                }

                $baseParams.DisplayName = $DisplayName
            }
            else {
                Write-PSFMessage -Level Host -Message "It seems that you didn't provide any Display Name. That would result in starting all services." -Exception $PSItem.Exception
                Stop-PSFFunction -Message "Stopping because of missing filters."
                return
            }
        }

        Invoke-TimeSignal -Start

        Get-Service @baseParams | Start-Service -ErrorAction SilentlyContinue -WarningAction $warningActionValue

        $service = Get-Service @baseParams | Select-Object @{Name = "Server"; Expression = { $Server } }, Name, Status, DisplayName
        $null = $output.Add($service)

        Invoke-TimeSignal -End
    }

    end {
        $output.ToArray() | Select-Object Server, DisplayName, Status, Name
    }
}


<#
    .SYNOPSIS
        Stop AX 2012 service(s)
         
    .DESCRIPTION
        Stop AX 2012 service(s) on the computer
         
    .PARAMETER Server
        Name of the computer(s) that you want to work against
         
        Default value is the name from the ComputerName from AxActiveAosConfiguration
         
    .PARAMETER DisplayName
        DisplayName of windows service that you want to work against
         
        Accepts wildcards for searching. E.g. -DisplayName "*ax*obj*"
         
    .PARAMETER Name
        Name of the Windows Service that you want to work against
         
        This parameter is used when piping in the details
         
        Designed to work together with the Get-AxEnvironment cmdlet
         
    .PARAMETER ShowOriginalProgress
        Instruct the cmdlet to output the status for the service
         
    .PARAMETER Force
        Instruct the cmdlet to force the stopping of the service
         
    .EXAMPLE
        PS C:\> Stop-AxService -Server TEST-AOS-01 -DisplayName *ax*obj*
         
        This will stop the service(s) that match the search pattern "*ax*obj*" on the server named "TEST-AOS-01".
         
    .EXAMPLE
        PS C:\> Stop-AxService -Server TEST-AOS-01 -DisplayName *ax*obj* -ShowOriginalProgress
         
        This will stop the service(s) that match the search pattern "*ax*obj*" on the server named "TEST-AOS-01".
        It will show the progress of stopping the service(s) in the console.
        It will show the status for the service(s) on the server afterwards.
         
    .EXAMPLE
        PS C:\> Get-AxEnvironment -ComputerName TEST-AOS-01 -Aos | Stop-AxEnvironment -ShowOriginalProgress
         
        This will scan the "TEST-AOS-01" server for all AOS instances (services) and stop them.
        It will show the progress of stopping the service(s) in the console.
        It will show the status for the service(s) on the server afterwards.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Stop-AxService {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = "Default")]
    param (

        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = "Pipeline")]
        [Parameter(ParameterSetName = "Default", Position = 1)]
        [Alias('ComputerName')]
        [string[]] $Server = $Script:ActiveAosComputername,

        [Parameter(Mandatory = $true, ParameterSetName = "Default", Position = 2)]
        [string] $DisplayName,

        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = "Pipeline")]
        [string[]] $Name,

        [switch] $ShowOriginalProgress,

        [switch] $Force

    )

    begin {
        $output = New-Object System.Collections.ArrayList

        $warningActionValue = "SilentlyContinue"
        if ($ShowOriginalProgress) { $warningActionValue = "Continue" }
    }

    process {
        $baseParams = @{ComputerName = $Server; ErrorAction = "SilentlyContinue" }

        if ($PSBoundParameters.ContainsKey("Name")) {
            foreach ($item in $Name) {
                if (($item.Trim().Length -eq 0) -or ($Name -eq "*")) {
                    Write-PSFMessage -Level Host -Message "It seems that you didn't provide any Name. That would result in shutting down all services." -Exception $PSItem.Exception
                    Stop-PSFFunction -Message "Stopping because of missing filters."
                    return
                }
            }

            if (Test-PSFFunctionInterrupt) { return }

            $baseParams.Name = $Name
        }
        else {
            if (($DisplayName.Length -gt 0) -and (-not($DisplayName -eq "*"))) {
                if ($DisplayName -notmatch "\*" ) {
                    $DisplayName = "*$DisplayName*"
                }

                $baseParams.DisplayName = $DisplayName
            }
            else {
                Write-PSFMessage -Level Host -Message "It seems that you didn't provide any Display Name. That would result in shutting down all services." -Exception $PSItem.Exception
                Stop-PSFFunction -Message "Stopping because of missing filters."
                return
            }
        }

        Invoke-TimeSignal -Start

        Write-PSFMessage -Level Verbose -Message "Stopping the specified services." -Target ((Convert-HashToArgString $baseParams) -join ",")
        Get-Service @baseParams | Stop-Service -Force:$Force -ErrorAction SilentlyContinue -WarningAction $warningActionValue

        $service = Get-Service @baseParams | Select-Object @{Name = "Server"; Expression = { $Server } }, Name, Status, DisplayName
        $null = $output.Add($service)

        Invoke-TimeSignal -End
    }

    end {
        $output.ToArray() | Select-Object Server, DisplayName, Status, Name
    }
}