commands.ps1


<#
    .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 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
       
    [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
    
    $RegOuter = Get-ItemProperty -Path $($RegKey.Name.Replace("HKEY_CURRENT_USER", "HKCU:"))

    $RegInner = Get-ItemProperty -Path (Join-Path $RegKey.Name $RegOuter.Current).Replace("HKEY_CURRENT_USER", "HKCU:")

    $RegInner.bindir
}


<#
    .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 {
    [CmdletBinding()]
    [OutputType([HashTable])]
    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 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, Position = 1)]
        [HashTable] $InputObject,

        [Parameter(Mandatory = $true, Position = 2)]
        [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 All
        Switch to instruct the cmdlet to return all services
         
    .PARAMETER Aos
        Switch to instruct the cmdlet to return AOS
         
    .PARAMETER ManagementReporter
        Switch to instruct the cmdlet to return ManagementReporter
         
    .PARAMETER DIXF
        Switch to 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] $All = [switch]::Present,

        [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") {
        $All = $false
    }

    Write-PSFMessage -Level Verbose -Message "The PSBoundParameters was" -Target $PSBoundParameters
    
    $MRService = "MR2012ProcessService"
    $dixfname = "Microsoft.Dynamics.AX.Framework.Tools.DMF.SSISHelperService.exe"

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

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

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

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

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

    $Services.ToArray()
}


<#
    .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-ModuleImport-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}
        
        $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 timing logic
         
    .DESCRIPTION
        Invoke timing logic that keeps track of the time spend inside a function
         
    .PARAMETER Start
        Switch to instruct the cmdlet that the starting of measurement
         
    .PARAMETER End
        Switch to 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
        Switch to 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 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
         
        Valid options are:
        "Leaf"
        "Container"
         
    .PARAMETER Create
        Switch to instruct the cmdlet to create the folder
         
    .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", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $True, Position = 1 )]
        [string[]] $Path,

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

        [switch] $Create
    )
    
    $res = $false

    $arrList = New-Object -TypeName "System.Collections.ArrayList"
         
    foreach ($item in $Path) {
        Write-PSFMessage -Level Verbose -Message "Testing the path: $item" -Target $item
        $temp = Test-Path -Path $item -Type $Type

        if ((!$temp) -and ($Create) -and ($Type -eq "Container")) {
            Write-PSFMessage -Level Verbose -Message "Creating the path: $item" -Target $item
            New-Item -Path $item -ItemType Directory -Force -ErrorAction Stop
            $temp = $true
        }
        elseif (!$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)) {
        Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1
    }
    else {
        $res = $true
    }

    $res
}


<#
    .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 GenerateScript
        Switch to 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:\> $Test = "1"
        PS C:\> Export-AxModelV2 -DatabaseServer "localhost" -ModelstoreDatabase "MicrosoftDynamicsAx_model" -Names "*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(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 1)]
        [string] $DatabaseServer = "localhost",

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)]
        [string] $ModelstoreDatabase = "MicrosoftDynamicsAx_model",
        
        [Parameter(Mandatory = $false, Position = 3)]
        [string] $Path = "c:\temp\ax2012.tools",

        [Parameter(Mandatory = $false, Position = 4)]
        [string] $Name = "*",

        [Parameter(Mandatory = $false, Position = 5)]
        [string] $Id = "*",

        [Parameter(Mandatory = $false, Position = 6)]
        [string] $Layer = "*",

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

    BEGIN {
        if (-not ($GenerateScript)) {
            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 ($GenerateScript) {
                    $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"
                    $null = Export-AXModel @params
                }
            }
            else {
                Write-PSFMessage -Level Verbose -Message "Skipping $filenameAxModel in layer $modelLayer"
            }
        }

        Invoke-TimeSignal -End
    }

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


<#
    .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(
        [Parameter()]
        [string] $Name = "*",
        
        [Parameter()]
        [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 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:RegistryClient

    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 the status of an AX 2012 environment
         
    .DESCRIPTION
        Get the status of AX 2012 services in your environment
         
    .PARAMETER ComputerName
        Name of the computer(s) that you want to work against
         
    .PARAMETER All
        Switch to instruct the cmdlet to include all known AX 2012 services
         
    .PARAMETER Aos
        Switch to instruct the cmdlet to include the AOS service
         
    .PARAMETER ManagementReporter
        Switch to instruct the cmdlet to include the ManagementReporter service
         
    .PARAMETER DIXF
        Switch to instruct the cmdlet to include the DIXF service
         
    .EXAMPLE
        PS C:\> Get-AxEnvironment
         
        This will get the status for all the default services from your environment.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-AxEnvironment {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [string[]] $ComputerName = @($env:computername),

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [switch] $All = [switch]::Present,

        [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") {
        $All = ![switch]::Present
    }

    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
    }

    $Params = Get-DeepClone $PSBoundParameters
    if ($Params.ContainsKey("ComputerName")) { $Params.Remove("ComputerName") }

    $Services = Get-ServiceList @Params
    
    $Results = foreach ($server in $ComputerName) {
        Write-PSFMessage -Level Verbose -Message "Working against: $server - listing services" -Target $server
        Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Select-Object @{Name = "Server"; Expression = {$Server}}, Name, Status, DisplayName
    }
    
    $Results | Select-Object Server, DisplayName, Status, Name
}


<#
    .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 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 CreateParents
        Switch to instruct the cmdlet to create missing parents on import
         
    .PARAMETER NoOptimize
        Switch to 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
        Switch to instruct the cmdlet not to prompt you with anything
         
    .PARAMETER GenerateScript
        Switch to 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".
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Import-AxModelV2 {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName="Generate")]
    Param(
        [Parameter(ValueFromPipelineByPropertyName, Mandatory = $false, ValueFromPipeline = $true, Position = 1)]
        [string] $DatabaseServer = "localhost",

        [Parameter(ValueFromPipelineByPropertyName, Mandatory = $false, ValueFromPipeline = $true, Position = 2)]
        [string] $ModelstoreDatabase = "MicrosoftDynamicsAx_model",
        
        [Parameter(Mandatory = $false, Position = 3)]
        [string] $Path = "c:\temp\ax2012.tools",

        [Parameter(Mandatory = $false, Position = 4)]
        [ValidateSet("Reject", "Push", "Overwrite")]
        [string] $ConflictMode = "Overwrite",
        
        [switch] $CreateParents,
        
        [switch] $NoOptimize,

        [switch] $NoPrompt,

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

    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")

        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 ($GenerateScript) {
                $arguments = Convert-HashToArgString -InputObject $clonedParams
                $argumentsSwitch = Convert-HashToArgStringSwitch -InputObject $paramsSwitch
                "Install-AxModel $($arguments -join ' ') $($argumentsSwitch -join ' ')"
            }
            else {
                #Install-AXModel @clonedParams @paramsSwitch
            }
        }
        
        Invoke-TimeSignal -End
    }

    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 GenerateScript
        Switch to instruct the cmdlet to only generate the needed command and not execute it
         
    .EXAMPLE
        PS C:\> Invoke-AxExportModelstore
         
        This will execute the cmdlet will 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 Invoke-AxExportModelstore {
    [CmdletBinding()]
    [OutputType([System.String], ParameterSetName = "Generate")]
    Param(
        [string] $DatabaseServer = "localhost",

        [string] $ModelstoreDatabase = "MicrosoftDynamicsAx_model",

        [string] $InstanceName,

        [string] $Suffix = $((Get-Date).ToString("yyyyMMdd")),
        
        [string] $Path = "c:\temp\ax2012.tools",

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

    Invoke-TimeSignal -Start

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

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

    $null = Import-Module $Script:AxPowerShellModule
        
    if ([System.String]::IsNullOrEmpty($InstanceName)) {
        $InstanceName = "{0}" -f $ModelstoreDatabase.Replace("_model", "")
    }

    $ExportPath = Join-Path $Path "$($InstanceName)_$($Suffix)"

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

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

        "Export-AxModelStore $($arguments -join ' ')"
    }
    else {
        Write-PSFMessage -Level Verbose -Message "Starting the export of the model store"
        
        Export-AxModelStore @params
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Start an AX 2012 environment
         
    .DESCRIPTION
        Start an AX 2012 services in your environment
         
    .PARAMETER ComputerName
        Name of the computer(s) that you want to work against
         
    .PARAMETER All
        Switch to instruct the cmdlet to include all known AX 2012 services
         
    .PARAMETER Aos
        Switch to instruct the cmdlet to include the AOS service
         
    .PARAMETER ManagementReporter
        Switch to instruct the cmdlet to include the ManagementReporter service
         
    .PARAMETER DIXF
        Switch to instruct the cmdlet to include the DIXF service
         
    .EXAMPLE
        PS C:\> Start-AxEnvironment
         
        This will start all the known AX 2012 services on the machine that you are executing it on.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Start-AxEnvironment {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [string[]] $ComputerName = @($env:computername),

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [switch] $All = [switch]::Present,

        [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") {
        $All = ![switch]::Present
    }

    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
    }

    $Params = Get-DeepClone $PSBoundParameters
    if ($Params.ContainsKey("ComputerName")) { $Params.Remove("ComputerName") }

    $Services = Get-ServiceList @Params
    
    $Results = foreach ($server in $ComputerName) {
        Write-PSFMessage -Level Verbose -Message "Working against: $server - starting services" -Target $server
        Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Start-Service -ErrorAction SilentlyContinue
    }

    $Results = foreach ($server in $ComputerName) {
        Write-PSFMessage -Level Verbose -Message "Working against: $server - listing services" -Target $server
        Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Select-Object @{Name = "Server"; Expression = {$Server}}, Name, Status, DisplayName
    }
    

    $Results | Select-Object Server, DisplayName, Status, Name
}


<#
    .SYNOPSIS
        Stop an AX 2012 environment
         
    .DESCRIPTION
        Stop an AX 2012 services in your environment
         
    .PARAMETER ComputerName
        Name of the computer(s) that you want to work against
         
    .PARAMETER All
        Switch to instruct the cmdlet to include all known AX 2012 services
         
    .PARAMETER Aos
        Switch to instruct the cmdlet to include the AOS service
         
    .PARAMETER ManagementReporter
        Switch to instruct the cmdlet to include the ManagementReporter service
         
    .PARAMETER DIXF
        Switch to instruct the cmdlet to include the DIXF service
         
    .EXAMPLE
        PS C:\> Stop-AxEnvironment
         
        This will stop all the known AX 2012 services on the machine that you are executing it on.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Stop-AxEnvironment {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [string[]] $ComputerName = @($env:computername),

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [switch] $All = [switch]::Present,

        [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") {
        $All = ![switch]::Present
    }

    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
    }

    $Params = Get-DeepClone $PSBoundParameters
    if ($Params.ContainsKey("ComputerName")) { $Params.Remove("ComputerName") }

    $Services = Get-ServiceList @Params
    
    $Results = foreach ($server in $ComputerName) {
        Write-PSFMessage -Level Verbose -Message "Working against: $server - stopping services" -Target $server
        Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Stop-Service -Force -ErrorAction SilentlyContinue
    }

    $Results = foreach ($server in $ComputerName) {
        Write-PSFMessage -Level Verbose -Message "Working against: $server - listing services" -Target $server
        Get-Service -ComputerName $server -Name $Services -ErrorAction SilentlyContinue | Select-Object @{Name = "Server"; Expression = {$Server}}, Name, Status, DisplayName
    }
    

    $Results | Select-Object Server, DisplayName, Status, Name
}