commands.ps1


<#
    .SYNOPSIS
        Assign D365 Security configuration
         
    .DESCRIPTION
        Assign the same security configuration as the ADMIN user in the D365FO database
         
    .PARAMETER sqlCommand
        The SQL Command object that should be used when assigning the permissions
         
    .PARAMETER Id
        Id of the user inside the D365FO database
         
    .EXAMPLE
        PS C:\> $SqlParams = @{
        DatabaseServer = "localhost"
        DatabaseName = "AXDB"
        SqlUser = "sqladmin"
        SqlPwd = "Pass@word1"
        TrustedConnection = $false
        }
         
        PS C:\> $SqlCommand = Get-SqlCommand @SqlParams
        PS C:\> Add-AadUserSecurity -SqlCommand $SqlCommand -Id "TestUser"
         
        This will create a new Sql Command object using the Get-SqlCommand cmdlet and the $SqlParams hashtable containing all the needed parameters.
        With the $SqlCommand in place it calls the Add-AadUserSecurity cmdlet and instructs it to update the "TestUser" to have the same security configuration as the ADMIN user.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Add-AadUserSecurity {
    [OutputType('System.Boolean')]
    param (
        [Parameter(Mandatory = $true)]
        [System.Data.SqlClient.SqlCommand] $SqlCommand,

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

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

    $null = $sqlCommand.Parameters.Add("@Id", $Id)

    Write-PSFMessage -Level Verbose -Message "Setting security roles in D365FO database"

    $differenceBetweenNewUserAndAdmin = $sqlCommand.ExecuteScalar()
    
    Write-PSFMessage -Level Verbose -Message "Difference between new user and admin security roles $differenceBetweenNewUserAndAdmin" -Target $differenceBetweenNewUserAndAdmin
    
    $SqlCommand.Parameters.Clear()

    $differenceBetweenNewUserAndAdmin -eq 0
}


<#
    .SYNOPSIS
        Add a certificate thumbprint to the wif.config
         
    .DESCRIPTION
        Register a certificate thumbprint in the wif.config file
         
    .PARAMETER CertificateThumbprint
        The thumbprint value of the certificate that you want to register in the wif.config file
         
    .EXAMPLE
        PS C:\> Add-WIFConfigAuthorityThumbprint -CertificateThumbprint "12312323r424"
         
        This will open the wif.config file and insert the "12312323r424" thumbprint value into the file.
         
    .NOTES
        Author: Kenny Saelen (@kennysaelen)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Add-WIFConfigAuthorityThumbprint
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$CertificateThumbprint
    )

    try
    {
        $wifConfigFile = Join-Path ([System.Environment]::ExpandEnvironmentVariables("%ServiceDrive%")) "\AOSService\webroot\wif.config"

        [xml]$wifXml = Get-Content $wifConfigFile

        $authorities = $wifXml.SelectNodes('//system.identityModel//identityConfiguration//securityTokenHandlers//securityTokenHandlerConfiguration//issuerNameRegistry//authority[@name="https://fakeacs.accesscontrol.windows.net/"]')
        
        if($authorities.Count -lt 1)
        {
            Write-PSFMessage -Level Critical -Message "Only one authority should be found with the name https://fakeacs.accesscontrol.windows.net/"
            Stop-PSFFunction -StepsUpward 1
            return
        }
        else
        {
            foreach ($authority in $authorities)
            {
               $addElem = $wifXml.CreateElement("add")
               $addAtt = $wifXml.CreateAttribute("thumbprint")
               $addAtt.Value = $CertificateThumbprint
               $addElem.Attributes.Append($addAtt)
               $authority.FirstChild.AppendChild($addElem)
               $wifXml.Save($wifConfigFile)
            }
        }
    }
    catch
    {
        Write-PSFMessage -Level Host -Message "Something went wrong while configuring the certificates and the Windows Identity Foundation configuration for the AOS" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }
}


<#
    .SYNOPSIS
        Backup a file
         
    .DESCRIPTION
        Backup a file in the same directory as the original file with a suffix
         
    .PARAMETER File
        Path to the file that you want to backup
         
    .PARAMETER Suffix
        The suffix value that you want to append to the file name when backing it up
         
    .EXAMPLE
        PS C:\> Backup-File -File c:\temp\d365fo.tools\test.txt -Suffix "Original"
         
        This will backup the "test.txt" file as "test_Original.txt" inside "c:\temp\d365fo.tools\"
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Backup-File {
    [CmdletBinding()]

    param (
        [Parameter(Mandatory = $true)]
        [string] $File,
        
        [Parameter(Mandatory = $true)]
        [string] $Suffix
        )

    $FileBackup = Get-BackupName $File $Suffix
    Write-PSFMessage -Level Verbose -Message "Backing up $File to $FileBackup" -Target (@($File, $FileBackup))
    (Get-Content -Path $File) | Set-Content -path $FileBackup
}


<#
    .SYNOPSIS
        Convert an object to boolean
         
    .DESCRIPTION
        Convert an object to boolean or default it to the specified boolean value
         
    .PARAMETER Object
        Input object that you want to work against
         
    .PARAMETER Default
        The default boolean value you want returned if the convert / cast fails
         
    .EXAMPLE
        PS C:\> ConvertTo-BooleanOrDefault -Object "1" -Default $true
         
        This will try and convert the "1" value to a boolean value.
        If the convert would fail, it would return the default value $true.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function ConvertTo-BooleanOrDefault {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')]
    [CmdletBinding()]
    [OutputType('System.Boolean')]
    param (
        [Object] $Object,

        [Boolean] $Default
    )

    [boolean] $result = $Default;
    $stringTrue = @("yes", "true", "ok", "y")

    $stringFalse = @( "no", "false", "n")

    try {
        if (-not ($null -eq $Object) ) {
            switch ($Object.ToString().ToLower()) {
                {$stringTrue -contains $_} {
                    $result = $true
                    break
                }
                {$stringFalse -contains $_} {
                    $result = $false
                    break
                }
                default {
                    $result = [System.Boolean]::Parser($Object.ToString())
                    break
                }
            }
        }
    }
    catch {
    }

    $result
}


<#
    .SYNOPSIS
        Load all necessary information about the D365 instance
         
    .DESCRIPTION
        Load all servicing dll files from the D365 instance into memory
         
    .EXAMPLE
        PS C:\> Get-ApplicationEnvironment
         
        This will load all the different dll files into memory.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-ApplicationEnvironment {
    $AOSPath = Join-Path ([System.Environment]::ExpandEnvironmentVariables("%ServiceDrive%")) "\AOSService\webroot\bin"
    
    Write-PSFMessage -Level Verbose -Message "Testing if we are running on a AOS server or not"
    if (-not (Test-Path -Path $AOSPath -PathType Container)) {
        $AOSPath = Join-Path ([System.Environment]::ExpandEnvironmentVariables("%ServiceDrive%")) "MRProcessService\MRInstallDirectory\Server\Services"

        Write-PSFMessage -Level Verbose -Message "Testing if we are running on a BI / MR server or not"
        if (-not (Test-Path -Path $AOSPath -PathType Container)) {
            Write-PSFMessage -Level Verbose -Message "It seems that you ran this cmdlet on a machine that doesn't have the assemblies needed to obtain system details. Most likely you ran it on a <c='em'>personal workstation / personal computer</c>."
            return
        }
    }

    $break = $false

    Write-PSFMessage -Level Verbose -Message "Shadow cloning all relevant assemblies to the Microsoft.Dynamics.ApplicationPlatform.Environment.dll to avoid locking issues. This enables us to install updates while having d365fo.tools loaded"

    $BasePath = "$AOSPath"
    [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList"
        
    $null = $Files2Process.Add("Microsoft.Dynamics.AX.Authentication.Instrumentation")
    $null = $Files2Process.Add("Microsoft.Dynamics.AX.Configuration.Base")
    $null = $Files2Process.Add("Microsoft.Dynamics.BusinessPlatform.SharedTypes")
    $null = $Files2Process.Add("Microsoft.Dynamics.AX.Framework.EncryptionEngine")
    $null = $Files2Process.Add("Microsoft.Dynamics.AX.Security.Instrumentation")
    $null = $Files2Process.Add("Microsoft.Dynamics.ApplicationPlatform.Environment")
        
    foreach ($name in $Files2Process) {
            
        $ShadowClone = Join-Path $BasePath "$name`_shadow.dll"
        $Path = Join-Path $BasePath "$name.dll"
            
        if (Test-Path -Path $Path -PathType Leaf) {
            Copy-Item -Path $Path -Destination $ShadowClone -Force

            $null = [AppDomain]::CurrentDomain.Load(([System.IO.File]::ReadAllBytes($ShadowClone)))

            Remove-Item -Path $ShadowClone -Force
        }
        else {
            Write-PSFMessage -Level Verbose -Message "Unable to load all needed files. Setting break variable."

            $break = $true
            break
        }
    }

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


<#
    .SYNOPSIS
        Get the Azure Service Objectives
         
    .DESCRIPTION
        Get the current tiering details from the Azure SQL Database instance
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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.
         
    .EXAMPLE
        PS C:\> Get-AzureServiceObjective -DatabaseServer dbserver1.database.windows.net -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123"
         
        This will get the Azure service objective details from the Azure SQL Database instance located at "dbserver1.database.windows.net"
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

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

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

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

        [Parameter(Mandatory = $true)]
        [string] $SqlPwd
    )
        
    $sqlCommand = Get-SqlCommand @PsBoundParameters -TrustedConnection $false

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

    $sqlCommand.CommandText = $commandText

    try {
        $sqlCommand.Connection.Open()

        Write-PSFMessage -Level Verbose "Execute the statement against the Azure DB instance" -Target $sqlCommand
        $reader = $sqlCommand.ExecuteReader()
        
        if ($reader.Read() -eq $true) {
            Write-PSFMessage -Level Verbose "Extracting details from the result retrieved from the Azure DB instance"

            $edition = $reader.GetString(1)
            $serviceObjective = $reader.GetString(2)

            $reader.close()
            
            $sqlCommand.Connection.Close()
            $sqlCommand.Dispose()
            
            [PSCustomObject]@{
                DatabaseEdition          = $edition
                DatabaseServiceObjective = $serviceObjective
            }
        }
        else {
            Write-PSFMessage -Level Host -Message "The query to detect <c='em'>edition</c> and <c='em'>service objectives</c> from the Azure DB instance <c='em'>failed</c>."
            Stop-PSFFunction -Message "Stopping because of missing parameters"
            return
        }
    }
    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
    }
}


<#
    .SYNOPSIS
        Get a backup name for the file
         
    .DESCRIPTION
        Generate a backup name for the file parsed
         
    .PARAMETER File
        Path to the file that you want a backup name for
         
    .PARAMETER Suffix
        The name that you want to put into the new backup file name
         
    .EXAMPLE
        PS C:\> Get-BackupName -File "C:\temp\d365do.tools\Test.txt" -Suffix "Original"
         
        The function will return "C:\temp\d365do.tools\Test_Original.txt"
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-BackupName {
    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true)]
        [string] $File,

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

    Write-PSFMessage -Level Verbose -Message "Getting backup name for file: $File" -Tag $File

    $FileInfo = [System.IO.FileInfo]::new($File)

    $BackupName = "{0}{1}_{2}{3}" -f $FileInfo.Directory, $FileInfo.BaseName, $Suffix, $FileInfo.Extension
    
    Write-PSFMessage -Level Verbose -Message "Backup name for the file will be $BackupName" -Tag $BackupName
    
    $BackupName
}


<#
    .SYNOPSIS
        Load the Canonical Identity Provider
         
    .DESCRIPTION
        Load the necessary dll files from the D365 instance to get the Canonical Identity Provider object
         
    .EXAMPLE
        PS C:\> Get-CanonicalIdentityProvider
         
        This will get the Canonical Identity Provider from the D365 instance
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-CanonicalIdentityProvider {
    [CmdletBinding()]
    param ()
    try {
        Write-PSFMessage -Level Verbose "Loading dll files to do some work against the CanonicalIdentityProvider."

        Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll"
        Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Security.AuthenticationCommon.dll"

        Write-PSFMessage -Level Verbose "Executing the CanonicalIdentityProvider lookup logic."
        $Identity = [Microsoft.Dynamics.AX.Security.AuthenticationCommon.AadHelper]::GetIdentityProvider()
        $Provider = [Microsoft.Dynamics.AX.Security.AuthenticationCommon.AadHelper]::GetCanonicalIdentityProvider($Identity)

        Write-PSFMessage -Level Verbose "CanonicalIdentityProvider is: $Provider" -Tag $Provider

        return $Provider
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while working against the CanonicalIdentityProvider." -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors"
        return
    }
}


<#
    .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 the identity provider
         
    .DESCRIPTION
        Execute a web request to get the identity provider for the given email address
         
    .PARAMETER Email
        Email address on the account that you want to get the Identity Provider details about
         
    .EXAMPLE
        PS C:\> Get-IdentityProvider -Email "Claire@contoso.com"
         
        This will get the Identity Provider details for the user account with the email address "Claire@contoso.com"
         
    .NOTES
        Author : Rasmus Andersen (@ITRasmus)
        Author : M�tz Jensen (@splaxi)
         
#>

function Get-IdentityProvider {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$Email
    )
    $tenant = Get-TenantFromEmail $Email

    try {
        $webRequest = New-WebRequest "https://login.windows.net/$tenant/.well-known/openid-configuration" $null "GET"

        $response = $WebRequest.GetResponse()

        if ($response.StatusCode -eq [System.Net.HttpStatusCode]::Ok) {

            $stream = $response.GetResponseStream()
    
            $streamReader = New-Object System.IO.StreamReader($stream);
        
            $openIdConfig = $streamReader.ReadToEnd()
            $streamReader.Close();
        }
        else {
            $statusDescription = $response.StatusDescription
            throw "Https status code : $statusDescription"
        }

        $openIdConfigJSON = ConvertFrom-Json $openIdConfig

        $openIdConfigJSON.issuer
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while executing the web request" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors"
        return
    }
}


<#
    .SYNOPSIS
        Get the instance provider from the D365FO instance
         
    .DESCRIPTION
        Get the instance provider from the dll files used for encryption and authentication for D365FO
         
    .EXAMPLE
        PS C:\> Get-InstanceIdentityProvider
         
        This will return the Instance Identity Provider based on the D365FO instance.
         
    .NOTES
        Author : Rasmus Andersen (@ITRasmus)
        Author : M�tz Jensen (@splaxi)
         
#>

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

    $files = @("$Script:AOSPath\bin\Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll",
        "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Security.AuthenticationCommon.dll")

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

    try {
        Add-Type -Path $files

        $Identity = [Microsoft.Dynamics.AX.Security.AuthenticationCommon.AadHelper]::GetIdentityProvider()
        
        Write-PSFMessage -Level Verbose -Message "The found instance identity provider is: $Identity" -Target $Identity

        $Identity
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while working against the Identity provider" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors"
        return
    }
}


<#
    .SYNOPSIS
        Get the Azure Database instance values
         
    .DESCRIPTION
        Extract the PlanId, TenantId and PlanCapability from the Azure Database instance
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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-InstanceValues -DatabaseServer SQLServer -DatabaseName AXDB -SqlUser "SqlAdmin" -SqlPwd "Pass@word1"
         
        This will extract the PlanId, TenantId and PlanCapability from the AXDB on the SQLServer, using the "SqlAdmin" credentials to do so.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-InstanceValues {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]
    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
    )
        
    $sqlCommand = Get-SqlCommand @PsBoundParameters

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

    $sqlCommand.CommandText = $commandText

    try {
        $sqlCommand.Connection.Open()

        Write-PSFMessage -Level Verbose "Execute the statement against the DB instance" -Target $sqlCommand
        $reader = $sqlCommand.ExecuteReader()
        
        if ($reader.Read() -eq $true) {
            Write-PSFMessage -Level Verbose "Extracting details from the result retrieved from the DB instance"

            $tenantId = $reader.GetString(0)
            $planId = $reader.GetGuid(1)
            $planCapability = $reader.GetString(2)

            @{
                TenantId       = $tenantId
                PlanId         = $planId
                PlanCapability = $planCapability
            }
        }
        else {
            Write-PSFMessage -Level Host -Message "The query to detect <c='em'>TenantId</c>, <c='em'>PlanId</c> and <c='em'>PlanCapability</c> from the database <c='em'>failed</c>."
            Stop-PSFFunction -Message "Stopping because of missing parameters"
            return
        }
    }
    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 {
        $reader.close()
            
        $sqlCommand.Connection.Close()
        $sqlCommand.Dispose()
    }
}


<#
    .SYNOPSIS
        Get the login name from the e-mail address
         
    .DESCRIPTION
        Extract the login name from the e-mail address by substring everything before the @ character
         
    .PARAMETER Email
        The e-mail address that you want to get the login name from
         
    .EXAMPLE
        PS C:\> Get-LoginFromEmail -Email Claire@contoso.com
         
        This will substring the e-mail address and return "Claire" as the result
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-LoginFromEmail {
    [CmdletBinding()]
    [OutputType('System.String')]
    param (
        [string]$Email
    )

    $email.Substring(0, $Email.LastIndexOf('@')).Trim()
}


<#
    .SYNOPSIS
        Get the network domain from the e-mail
         
    .DESCRIPTION
        Get the network domain provider (Azure) for the e-mail / user
         
    .PARAMETER Email
        The e-mail that you want to retrieve the provider for
         
    .EXAMPLE
        PS C:\> Get-NetworkDomain -Email "Claire@contoso.com"
         
        This will return the provider registered with the "Claire@contoso.com" e-mail address.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-NetworkDomain {
    [CmdletBinding()]
    [OutputType('System.String')]
    param(
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$Email
    )

    $tenant = Get-TenantFromEmail $Email
    $provider = Get-InstanceIdentityProvider
    $canonicalIdentityProvider = Get-CanonicalIdentityProvider

    if ($Provider.ToLower().Contains($Tenant.ToLower()) -eq $True) {
        $canonicalIdentityProvider
    }
    else {
        "$canonicalIdentityProvider$Tenant"
    }
}


<#
    .SYNOPSIS
        Get the product information
         
    .DESCRIPTION
        Get the product information object from the environment
         
    .EXAMPLE
        PS C:\> Get-ProductInfoProvider
         
        This will get the product information object and return it
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-ProductInfoProvider {
    Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Provider.dll"
    [Microsoft.Dynamics.BusinessPlatform.ProductInformation.Provider.ProductInfoProvider]::get_Provider()
}


<#
    .SYNOPSIS
        Get the list of Dynamics 365 services
         
    .DESCRIPTION
        Get the list of Dynamics 365 service names based on the parameters
         
    .PARAMETER All
        Switch to instruct the cmdlet to output all service names
         
    .PARAMETER Aos
        Switch to instruct the cmdlet to output the aos service name
         
    .PARAMETER Batch
        Switch to instruct the cmdlet to output the batch service name
         
    .PARAMETER FinancialReporter
        Switch to instruct the cmdlet to output the financial reporter service name
         
    .PARAMETER DMF
        Switch to instruct the cmdlet to output the data management service name
         
    .EXAMPLE
        PS C:\> Get-ServiceList -All
         
        This will return all services for an D365 environment
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Get-ServiceList {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [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] $Batch,

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

        [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 5 )]
        [switch] $DMF
    )

    if ($PSCmdlet.ParameterSetName -eq "Specific") {
        $All = ![switch]::Present
    }

    Write-PSFMessage -Level Verbose -Message "The PSBoundParameters was" -Target $PSBoundParameters

    $aosname = "w3svc"
    $batchname = "DynamicsAxBatch"
    $financialname = "MR2012ProcessService"
    $dmfname = "Microsoft.Dynamics.AX.Framework.Tools.DMF.SSISHelperService.exe"

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

    if ($All.IsPresent) {
        $null = $Services.AddRange(@($aosname, $batchname, $financialname, $dmfname))
    }
    else {
        if ($Aos.IsPresent) {
            $null = $Services.Add($aosname)
        }
        if ($Batch.IsPresent) {
            $null = $Services.Add($batchname)
        }
        if ($FinancialReporter.IsPresent) {
            $null = $Services.Add($financialname)
        }
        if ($DMF.IsPresent) {
            $null = $Services.Add($dmfname)
        }
    }

    $Services.ToArray()
}


<#
    .SYNOPSIS
        Get a SqlCommand object
         
    .DESCRIPTION
        Get a SqlCommand object initialized with the passed parameters
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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 AxDB -SqlUser User123 -SqlPwd "Password123" -TrustedConnection $true
         
        This will initialize a new SqlCommand object (.NET type) with localhost as the server name, AxDB as the database and the User123 sql credentials.
         
    .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='d365fo.tools'")
    
    Write-PSFMessage -Level Verbose -Message "Building the SQL connection string." -Target $Params
    $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) {
        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 the tenant from e-mail address
         
    .DESCRIPTION
        Get the tenant (domain) from an e-mail address
         
    .PARAMETER Email
        The e-mail address you want to get the tenant from
         
    .EXAMPLE
        PS C:\> Get-TenantFromEmail -Email "Claire@contoso.com"
         
        This will return the tenant (domain) from the "Claire@contoso.com" e-mail address.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-TenantFromEmail {
    [CmdletBinding()]
    [OutputType('System.String')]
    param (
        [string] $email
    )

    $email.Substring($email.LastIndexOf('@') + 1).Trim();
}


<#
    .SYNOPSIS
        Get the SID from an Azure Active Directory (AAD) user
         
    .DESCRIPTION
        Get the generated SID that an Azure Active Directory (AAD) user will get in relation to Dynamics 365 Finance & Operations environment
         
    .PARAMETER SignInName
        The sign in name (email address) for the user that you want the SID from
         
    .PARAMETER Provider
        The provider connected to the sign in name
         
    .EXAMPLE
        PS C:\> Get-UserSIDFromAad -SignInName "Claire@contoso.com" -Provider "ZXY"
         
        This will get the SID for Azure Active Directory user "Claire@contoso.com"
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-UserSIDFromAad {
    [CmdletBinding()]
    [OutputType('System.String')]
    param     (
        [string] $SignInName,
        
        [string] $Provider
    )

    try {

        Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.BusinessPlatform.SharedTypes.dll"
        Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.ApplicationPlatform.PerformanceCounters.dll"
        Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll"
        Add-Type -Path "$Script:AOSPath\bin\Microsoft.Dynamics.AX.Security.SidGenerator.dll"

        $SID = [Microsoft.Dynamics.Ax.Security.SidGenerator]::Generate($SignInName, $Provider)
        Write-PSFMessage -Level Verbose -Message "Generated SID: $SID" -Target $SID

        $SID

    }
    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
    }
}


<#
    .SYNOPSIS
        Import an Azure Active Directory (AAD) user
         
    .DESCRIPTION
        Import an Azure Active Directory (AAD) user into a Dynamics 365 for Finance & Operations environment
         
    .PARAMETER SqlCommand
        The SQL Command object that should be used when importing the AAD user
         
    .PARAMETER SignInName
        The sign in name (email address) for the user that you want to import
         
    .PARAMETER Name
        The name that the imported user should have inside the D365FO environment
         
    .PARAMETER Id
        The ID that the imported user should have inside the D365FO environment
         
    .PARAMETER SID
        The SID that correlates to the imported user inside the D365FO environment
         
    .PARAMETER StartUpCompany
        The default company (legal entity) for the imported user
         
    .PARAMETER IdentityProvider
        The provider for the imported to validated against
         
    .PARAMETER NetworkDomain
        The network domain of the imported user
         
    .PARAMETER ObjectId
        The Azure Active Directory object id for the imported user
         
    .EXAMPLE
        PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123"
        PS C:\> Import-AadUserIntoD365FO -SqlCommand $SqlCommand -SignInName "Claire@contoso.com" -Name "Claire" -Id "claire" -SID "123XYZ" -StartupCompany "DAT" -IdentityProvider "XYZ" -NetworkDomain "Contoso.com" -ObjectId "123XYZ"
         
        This will get a SqlCommand object that will connect to the localhost server and the AXDB database, with the sql credential "User123".
        The SqlCommand object is passed to the Import-AadUserIntoD365FO along with all the necessary details for importing Claire@contoso.com as an user into the D365FO environment.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Import-AadUserIntoD365FO {
    [CmdletBinding()]
    param
    (
        [string] $SqlCommand,

        [string] $SignInName,

        [string] $Name,

        [string] $Id,

        [string] $SID,

        [string] $StartUpCompany,

        [string] $IdentityProvider,

        [string] $NetworkDomain,

        [string] $ObjectId
    )

    Write-PSFMessage -Level Verbose -Message "Testing the Email $signInName" -Target $signInName
    
    $UserFound = Test-AadUserInD365FO $sqlCommand $SignInName

    if ($UserFound -eq $false) {

        Write-PSFMessage -Level Verbose -Message "Testing the userid $Id" -Target $Id

        $idTaken = Test-AadUserIdInD365FO $sqlCommand $id

        if (Test-PSFFunctionInterrupt) { return }

        if ($idTaken -eq $false) {

            $userAdded = New-D365FOUser $sqlCommand $SignInName $Name $Id $Sid $StartUpCompany $IdentityProvider $NetworkDomain $ObjectId
        
            if ($userAdded -eq $true) {

                $securityAdded = Add-AadUserSecurity $sqlCommand $Id

                if ($securityAdded -eq $false) {
                    Write-PSFMessage -Level Host -Message "User $SignInName did not get securityRoles"
                    Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
                    return
                }
            }
            else {
                Write-PSFMessage -Level Host -Message "User $SignInName, not added to D365FO"
                Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
                return
            }
        }
        else {
            Write-PSFMessage -Level Host -Message "An User with ID = '$ID' allready exists"
            Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
            return
        }

    }
    else {
        Write-PSFMessage -Level Host -Message "An User with Email $SignInName already exists in D365FO"
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }
}


<#
    .SYNOPSIS
        Create a database copy in Azure SQL Database instance
         
    .DESCRIPTION
        Create a new database by cloning a database in Azure SQL Database instance
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN)
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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 NewDatabaseName
        Name of the new / cloned database in the Azure SQL Database instance
         
    .EXAMPLE
        PS C:\> Invoke-AzureBackupRestore -DatabaseServer TestServer.database.windows.net -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" -NewDatabaseName ExportClone
         
        This will create a database named "ExportClone" in the "TestServer.database.windows.net" Azure SQL Database instance.
        It uses the SQL credential "User123" to preform the needed actions.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Invoke-AzureBackupRestore {
    [CmdletBinding()]
    [OutputType('System.Boolean')]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DatabaseServer,

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

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

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

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

    Invoke-TimeSignal -Start

    $StartTime = Get-Date
    
    $SqlConParams = @{DatabaseServer = $DatabaseServer; SqlUser = $SqlUser; SqlPwd = $SqlPwd; TrustedConnection = $false}
    $sqlCommand = Get-SqlCommand @SqlConParams -DatabaseName $DatabaseName
    
    $commandText = (Get-Content "$script:ModuleRoot\internal\sql\newazuredbfromcopy.sql") -join [Environment]::NewLine
    
    $commandText = $commandText.Replace('@CurrentDatabase', $DatabaseName)
    $commandText = $commandText.Replace('@NewName', $NewDatabaseName)

    $sqlCommand.CommandText = $commandText

    try {
        $sqlCommand.Connection.Open()

        Write-PSFMessage -Level Verbose -Message "Will execute the following command: $commandText" -Target $commandText
        
        $null = $sqlCommand.ExecuteNonQuery()
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while creating the copy of the Azure DB" -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()
    }
   
    $sqlCommand = Get-SqlCommand @SqlConParams -DatabaseName "master"

    $commandText = (Get-Content "$script:ModuleRoot\internal\sql\checkfornewazuredb.sql") -join [Environment]::NewLine

    $sqlCommand.CommandText = $commandText

    $null = $sqlCommand.Parameters.Add("@NewName", $NewDatabaseName)
    $null = $sqlCommand.Parameters.Add("@Time", $StartTime)

    try {
        $sqlCommand.Connection.Open()

        $operation_row_count = 0
        #Loop every minute until we get a row, if we get a row copy is done
        while ($operation_row_count -eq 0) {
            Write-PSFMessage -Level Verbose -Message "Waiting for the creation of the copy."
            $Reader = $sqlCommand.ExecuteReader()
            $Datatable = New-Object System.Data.DataTable
            $Datatable.Load($Reader)
            $operation_row_count = $Datatable.Rows.Count
            Start-Sleep -s 60
        }

        $true
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while checking for the new copy of the Azure DB" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }
    finally {
        $Reader.close()

        if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
            $sqlCommand.Connection.Close()
        }

        $sqlCommand.Dispose()
        $Datatable.Dispose()
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Clear Azure SQL Database specific objects
         
    .DESCRIPTION
        Clears all the objects that can only exists inside an Azure SQL Database instance or disable things that will require rebuilding on the receiving system
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN)
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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
         
    .EXAMPLE
        PS C:\> Invoke-ClearAzureSpecificObjects -DatabaseServer TestServer.database.windows.net -DatabaseName ExportClone -SqlUser User123 -SqlPwd "Password123"
         
        This will execute all necessary scripts against the "ExportClone" database that exists in the "TestServer.database.windows.net" Azure SQL Database instance.
        It uses the SQL credential "User123" to preform the needed actions.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Invoke-ClearAzureSpecificObjects {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DatabaseServer,

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

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

        [Parameter(Mandatory = $true)]
        [string] $SqlPwd
    )
        
    $sqlCommand = Get-SQLCommand @PsBoundParameters -TrustedConnection $false

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

    $commandText = $commandText.Replace("@NewDatabase", $DatabaseName)
    
    $sqlCommand.CommandText = $commandText

    try {
        $sqlCommand.Connection.Open()

        $null = $sqlCommand.ExecuteNonQuery()

        $true
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while clearing the Azure specific objects from the Azure DB" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }
    finally {
        if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
            $sqlCommand.Connection.Close()
        }

        $sqlCommand.Dispose()
    }
}


<#
    .SYNOPSIS
        Clear SQL Server (on-premises) specific objects
         
    .DESCRIPTION
        Clears all the objects that can only exists inside a SQL Server (on-premises) instance or disable things that will require rebuilding on the receiving system
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN)
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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:\> Invoke-ClearSqlSpecificObjects -DatabaseServer localhost -DatabaseName ExportClone -SqlUser User123 -SqlPwd "Password123"
         
        This will execute all necessary scripts against the "ExportClone" database that exists in the localhost SQL Server instance.
        It uses the SQL credential "User123" to preform the needed actions.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Invoke-ClearSqlSpecificObjects {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    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
    )
    
    $sqlCommand = Get-SQLCommand @PsBoundParameters

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

    $sqlCommand.CommandText = $commandText

    try {
        $sqlCommand.Connection.Open()

        $null = $sqlCommand.ExecuteNonQuery()

        $true
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }
    finally {
        if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
            $sqlCommand.Connection.Close()
        }

        $sqlCommand.Dispose()
    }
}


<#
    .SYNOPSIS
        Backup & Restore SQL Server database
         
    .DESCRIPTION
        Backup a database and restore it back into the SQL Server
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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
         
    .PARAMETER NewDatabaseName
        Name of the new (restored) database
         
    .PARAMETER BackupDirectory
        Path to a directory that can store the backup file
         
    .EXAMPLE
        PS C:\> Invoke-SqlBackupRestore -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123" -NewDatabaseName "ExportClone" -BackupDirectory "C:\temp\d365fo.tools\sqlbackup"
         
        This will backup the AxDB database and place the backup file inside the "c:\temp\d365fo.tools\sqlbackup" directory.
        The backup file will the be used to restore into a new database named "ExportClone".
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

Function Invoke-SqlBackupRestore {
    [CmdletBinding()]
    [OutputType('System.Boolean')]
    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,

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

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

    Invoke-TimeSignal -Start

    $Params = @{DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd; TrustedConnection = $TrustedConnection;
    }

    $sqlCommand = Get-SQLCommand @Params

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\backuprestoredb.sql") -join [Environment]::NewLine
    $null = $sqlCommand.Parameters.Add("@CurrentDatabase", $DatabaseName)
    $null = $sqlCommand.Parameters.Add("@NewName", $NewDatabaseName)
    $null = $sqlCommand.Parameters.Add("@BackupDirectory", $BackupDirectory)

    try {
        $sqlCommand.Connection.Open()

        Write-PSFMessage -Level Verbose -Message "Executing the statement against the SQL Server" -Target $sqlCommand.CommandText
        $null = $sqlCommand.ExecuteNonQuery()
        
        $true
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }
    finally {
        
        $sqlCommand.Connection.Close()
        $sqlCommand.Dispose()
    }

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Invoke the sqlpackage executable
         
    .DESCRIPTION
        Invoke the sqlpackage executable and pass the necessary parameters to it
         
    .PARAMETER Action
        Can either be import or export
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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 sqlpackage work with TrustedConnection or not
         
    .PARAMETER FilePath
        Path to the file, used for either import or export
         
    .PARAMETER Properties
        Array of all the properties that needs to be parsed to the sqlpackage.exe
         
    .EXAMPLE
        PS C:\> $BaseParams = @{
        DatabaseServer = $DatabaseServer
        DatabaseName = $DatabaseName
        SqlUser = $SqlUser
        SqlPwd = $SqlPwd
        }
         
        PS C:\> $ImportParams = @{
        Action = "import"
        FilePath = $BacpacFile
        }
         
        PS C:\> Invoke-SqlPackage @BaseParams @ImportParams
         
        This will start the sqlpackage.exe file and pass all the needed parameters.
         
    .NOTES
        Author: M�tz Jensen (@splaxi)
         
#>

function Invoke-SqlPackage {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [ValidateSet('Import', 'Export')]
        [string]$Action,
        
        [string]$DatabaseServer,
        
        [string]$DatabaseName,
        
        [string]$SqlUser,
        
        [string]$SqlPwd,
        
        [string]$TrustedConnection,
        
        [string]$FilePath,
        
        [string[]]$Properties
    )
              
    $executable = $Script:SqlPackage

    Invoke-TimeSignal -Start

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

    Write-PSFMessage -Level Verbose -Message "Starting to prepare the parameters for sqlpackage.exe"

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

    if ($Action -eq "export") {
        $null = $Params.Add("/Action:export")
        $null = $Params.Add("/SourceServerName:$DatabaseServer")
        $null = $Params.Add("/SourceDatabaseName:$DatabaseName")
        $null = $Params.Add("/TargetFile:$FilePath")
        $null = $Params.Add("/Properties:CommandTimeout=1200")
    
        if (!$UseTrustedConnection) {
            $null = $Params.Add("/SourceUser:$SqlUser")
            $null = $Params.Add("/SourcePassword:$SqlPwd")
        }
        
        Remove-Item -Path $FilePath -ErrorAction SilentlyContinue -Force
    }
    else {
        $null = $Params.Add("/Action:import")
        $null = $Params.Add("/TargetServerName:$DatabaseServer")
        $null = $Params.Add("/TargetDatabaseName:$DatabaseName")
        $null = $Params.Add("/SourceFile:$FilePath")
        $null = $Params.Add("/Properties:CommandTimeout=1200")
        
        if (!$UseTrustedConnection) {
            $null = $Params.Add("/TargetUser:$SqlUser")
            $null = $Params.Add("/TargetPassword:$SqlPwd")
        }
    }

    foreach ($item in $Properties) {
        $null = $Params.Add("/Properties:$item")
    }

    Write-PSFMessage -Level Verbose "Start sqlpackage.exe with parameters" -Target $Params
    
    #! We should consider to redirect the standard output & error like this: https://stackoverflow.com/questions/8761888/capturing-standard-out-and-error-with-start-process
    Start-Process -FilePath $executable -ArgumentList ($Params -join " ") -NoNewWindow -Wait
    
    Invoke-TimeSignal -End
    
    $true
}


<#
    .SYNOPSIS
        Handle time measurement
         
    .DESCRIPTION
        Handle time measurement from when a cmdlet / function starts and ends
         
        Will write the output to the verbose stream (Write-PSFMessage -Level Verbose)
         
    .PARAMETER Start
        Switch to instruct the cmdlet that a start time registration needs to take place
         
    .PARAMETER End
        Switch to instruct the cmdlet that a time registration has come to its end and it needs to do the calculation
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -Start
         
        This will start the time measurement for any given cmdlet / function
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -End
         
        This will end the time measurement for any given cmdlet / function.
        The output will go into the verbose stream.
         
    .NOTES
        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) {
        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 authorization header
         
    .DESCRIPTION
        Get a new authorization header by acquiring a token from the authority web service
         
    .PARAMETER Authority
        The authority that you want to work against
         
    .PARAMETER ClientId
        The client id that you have registered for getting access to the web resource that you want to work against
         
    .PARAMETER ClientSecret
        The client secret that enables you to prove that you have privileges to get an authorization header
         
    .PARAMETER D365FO
        The URL to the Dynamics 365 for Finance & Operations that you want to work against
         
    .EXAMPLE
        PS C:\> New-AuthorizationHeader -Authority "XYZ" -ClientId "123" -ClientSecret "TopSecretId" -D365FO "https://usnconeboxax1aos.cloud.onebox.dynamics.com"
         
        This will retrieve a new authorization header from the D365FO instance located at "https://usnconeboxax1aos.cloud.onebox.dynamics.com".
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function New-AuthorizationHeader {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    Param (
        [string] $Authority,
        [string] $ClientId,
        [string] $ClientSecret,
        [string] $D365FO
    )
    
    $authContext = new-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext ($Authority, $false)

    $clientCred = New-Object  Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential($ClientId, $ClientSecret)

    $task = $authContext.AcquireTokenAsync($D365FO, $clientCred)

    $taskStatus = $task.Wait(1000)

    Write-PSFMessage -Level Verbose -Message "Status $TaskStatus"

    $authorizationHeader = $task.Result

    Write-PSFMessage -Level Verbose -Message "AuthorizationHeader $authorizationHeader"

    $authorizationHeader
}


<#
    .SYNOPSIS
        Creates a new user
         
    .DESCRIPTION
        Creates a new user in a Dynamics 365 for Finance & Operations instance
         
    .PARAMETER sqlCommand
        The SQL Command object that should be used when creating the new user
         
    .PARAMETER SignInName
        The sign in name (email address) for the user that you want the SID from
         
    .PARAMETER Name
        The name that the imported user should have inside the D365FO environment
         
    .PARAMETER Id
        The ID that the imported user should have inside the D365FO environment
         
    .PARAMETER SID
        The SID that correlates to the imported user inside the D365FO environment
         
    .PARAMETER StartUpCompany
        The default company (legal entity) for the imported user
         
    .PARAMETER IdentityProvider
        The provider for the imported to validated against
         
    .PARAMETER NetworkDomain
        The network domain of the imported user
         
    .PARAMETER ObjectId
        The Azure Active Directory object id for the imported user
         
    .EXAMPLE
        PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123"
        PS C:\> New-D365FOUser -SqlCommand $SqlCommand -SignInName "Claire@contoso.com" -Name "Claire" -Id "claire" -SID "123XYZ" -StartupCompany "DAT" -IdentityProvider "XYZ" -NetworkDomain "Contoso.com" -ObjectId "123XYZ"
         
        This will get a SqlCommand object that will connect to the localhost server and the AXDB databae, with the sql credential "User123".
        The SqlCommand object is passed to the Import-AadUserIntoD365FO along with all the necessary details for importing Claire@contoso.com as an user into the D365FO environment.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: Rasmus Andersen (@ITRasmus)
         
#>

function New-D365FOUser {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    Param (
        [string] $SqlCommand,
        [string] $SignInName,
        [string] $Name,
        [string] $Id,
        [string] $SID,
        [string] $StartUpCompany,
        [string] $IdentityProvider,
        [string] $NetworkDomain,
        [string] $ObjectId
    )
    
    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\Add-AadUserIntoD365FO.sql") -join [Environment]::NewLine

    Write-PSFMessage -Level Verbose -Message "Adding User : $SignInName,$Name,$Id,$SID,$StartUpCompany,$IdentityProvider,$NetworkDomain"

    $null = $sqlCommand.Parameters.Add("@SignInName", $SignInName)
    $null = $sqlCommand.Parameters.Add("@Name", $Name)
    $null = $sqlCommand.Parameters.Add("@SID", $SID)
    $null = $sqlCommand.Parameters.Add("@NetworkDomain", $NetworkDomain)
    $null = $sqlCommand.Parameters.Add("@IdentityProvider", $IdentityProvider)
    $null = $sqlCommand.Parameters.Add("@Id", $Id)
    $null = $sqlCommand.Parameters.Add("@ObjectId", $ObjectId)

    Write-PSFMessage -Level Verbose -Message "Creating the user in database"

    $rowsCreated = $sqlCommand.ExecuteScalar()
    
    Write-PSFMessage -Level Verbose -Message "Rows inserted $rowsCreated for user $SignInName"
    
    $SqlCommand.Parameters.Clear()

    $rowsCreated -eq 1
}


<#
    .SYNOPSIS
        Create a new self signed certificate
         
    .DESCRIPTION
        Create a new self signed certificate and have it password protected
         
    .PARAMETER CertificateFileName
        Path to the location where you want to store the CER file for the certificate
         
    .PARAMETER PrivateKeyFileName
        Path to the location where you want to store the PFX file for the certificate
         
    .PARAMETER Password
        The password that you want to use to protect your different certificates with
         
    .EXAMPLE
        PS C:\> New-D365SelfSignedCertificate -CertificateFileName "C:\temp\d365fo.tools\TestAuth.cer" -PrivateKeyFileName "C:\temp\d365fo.tools\TestAuth.pfx" -Password (ConvertTo-SecureString -String "pass@word1" -Force -AsPlainText)
         
        This will generate a new CER certificate that is stored at "C:\temp\d365fo.tools\TestAuth.cer".
        This will generate a new PFX certificate that is stored at "C:\temp\d365fo.tools\TestAuth.pfx".
        Both certificates will be password protected with "pass@word1".
         
    .NOTES
        Author: Kenny Saelen (@kennysaelen)
        Author: M�tz Jensen (@Splaxi)
         
#>

function New-D365SelfSignedCertificate {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 1)]
        [string] $CertificateFileName = (Join-Path $env:TEMP "TestAuthCert.cer"),

        [Parameter(Mandatory = $false, Position = 2)]
        [string] $PrivateKeyFileName = (Join-Path $env:TEMP "TestAuthCert.pfx"),

        [Parameter(Mandatory = $false, Position = 3)]
        [Security.SecureString] $Password = (ConvertTo-SecureString -String "Password1" -Force -AsPlainText)
    )

    try {
        # First generate a self-signed certificate and place it in the local store on the machine
        $certificate = New-SelfSignedCertificate -dnsname 127.0.0.1 -CertStoreLocation cert:\LocalMachine\My -FriendlyName "D365 Automated testing certificate" -Provider "Microsoft Strong Cryptographic Provider"
        $certificatePath = 'cert:\localMachine\my\' + $certificate.Thumbprint

        # Export the private key
        Export-PfxCertificate -cert $certificatePath -FilePath $PrivateKeyFileName -Password $Password

        # Import the certificate into the local machine's trusted root certificates store
        $importedCertificate = Import-PfxCertificate -FilePath $PrivateKeyFileName -CertStoreLocation Cert:\LocalMachine\Root -Password $Password
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while generating the self-signed certificate and installing it into the local machine's trusted root certificates store." -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }

    return $importedCertificate
}


<#
    .SYNOPSIS
        Decrypt web.config file
         
    .DESCRIPTION
        Utilize the built in encryptor utility to decrypt the web.config file from inside the AOS
         
    .PARAMETER File
        Path to the file that you want to work against
         
        Please be careful not to point to the original file from inside the AOS directory
         
    .PARAMETER DropPath
        Path to the directory where you want save the file after decryption is completed
         
    .EXAMPLE
        PS C:\> New-DecryptedFile -File "C:\temp\d365fo.tools\web.config" -DropPath "c:\temp\d365fo.tools\decrypted.config"
         
        This will take the "C:\temp\d365fo.tools\web.config" and decrypt it.
        After decryption the output file will be stored in "c:\temp\d365fo.tools\decrypted.config".
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function New-DecryptedFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    Param (
        [string] $File,
        
        [string] $DropPath
    )
    
    $Decrypter = Join-Path  $AosServiceWebRootPath -ChildPath "bin\Microsoft.Dynamics.AX.Framework.ConfigEncryptor.exe"

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

    $fileInfo = [System.IO.FileInfo]::new($File)
    $DropFile = Join-Path $DropPath $FileInfo.Name
    
    Write-PSFMessage -Level Verbose -Message "Extracted file path is: $DropFile" -Target $DropFile
    Copy-Item $File $DropFile -Force -ErrorAction Stop

    if (-not (Test-PathExists -Path $DropFile -Type Leaf)) { return }
    
    & $Decrypter -decrypt $DropFile
}


<#
    .SYNOPSIS
        Get a web request object
         
    .DESCRIPTION
        Get a prepared web request object with all necessary headers and tokens in place
         
    .PARAMETER RequestUrl
        The URL you want to work against
         
    .PARAMETER AuthorizationHeader
        The Authorization Header object that you want to use for you web request
         
    .PARAMETER Action
        The HTTP action you want to preform
         
    .EXAMPLE
        PS C:\> New-WebRequest -RequestUrl "https://login.windows.net/contoso/.well-known/openid-configuration" -AuthorizationHeader $null -Action GET
         
        This will create a new web request object that will work against the "https://login.windows.net/contoso/.well-known/openid-configuration" URL.
        The HTTP action is GET and in this case we don't need an Authorization Header in place.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function New-WebRequest {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]

    param    (
        $RequestUrl,
        $AuthorizationHeader,
        $Action
    )
    
    Write-PSFMessage -Level Verbose -Message "New Request $RequestUrl, $Action"
    $request = [System.Net.WebRequest]::Create($RequestUrl)

    if ($null -ne $AuthorizationHeader) {
        $request.Headers["Authorization"] = $AuthorizationHeader.CreateAuthorizationHeader()
    }

    $request.Method = $Action
    
    $request
}


<#
    .SYNOPSIS
        Rename the value in the web.config file
         
    .DESCRIPTION
        Replace the old value with the new value inside a web.config file
         
    .PARAMETER File
        Path to the file that you want to update/rename/replace
         
    .PARAMETER NewValue
        The new value that replaces the old value
         
    .PARAMETER OldValue
        The old value that needs to be replaced
         
    .EXAMPLE
        PS C:\> Rename-ConfigValue -File "C:\temp\d365fo.tools\web.config" -NewValue "Demo-8.1" -OldValue "usnconeboxax1aos"
         
        This will open the "C:\temp\d365fo.tools\web.config" file and replace all "usnconeboxax1aos" entries with "Demo-8.1"
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Rename-ConfigValue {
    param (
        [string] $File,
        [string] $NewValue,
        [string] $OldValue
    )

    Write-PSFMessage -Level Verbose -Message "Replace content from $File. Old value is $OldValue. New value is $NewValue." -Target (@($File, $OldValue, $NewValue))
    
    (Get-Content $File).replace($OldValue, $NewValue) | Set-Content $File
}


<#
    .SYNOPSIS
        Short description
         
    .DESCRIPTION
        Long description
         
    .PARAMETER InputObject
        Parameter description
         
    .PARAMETER Property
        Parameter description
         
    .PARAMETER ExcludeProperty
        Parameter description
         
    .PARAMETER TypeName
        Parameter description
         
    .EXAMPLE
        PS C:\> Select-DefaultView -InputObject $result -Property CommandName, Synopsis
         
        This will help you do it right.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

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

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


<#
    .SYNOPSIS
        Provision an user to be the administrator of a Dynamics 365 for Finance & Operations environment
         
    .DESCRIPTION
        Provision an user to be the administrator by using the supplied tools from Microsoft (AdminUserProvisioning.exe)
         
    .PARAMETER SignInName
        The sign in name (email address) for the user that you want to be the administrator
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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.
         
    .EXAMPLE
        PS C:\> Set-AdminUser -SignInName "Claire@contoso.com" -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123"
         
        This will provision the user with the e-mail "Claire@contoso.com" to be the administrator of the D365 for Finance & Operations instance.
        It will handle if the tenant is switching also, and update the necessary details.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Set-AdminUser {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    Param (
        [string] $SignInName,
        [string] $DatabaseServer,
        [string] $DatabaseName,
        [string] $SqlUser,
        [string] $SqlPwd
    )

    $WebConfigFile = Join-Path $Script:AOSPath $Script:WebConfig

    $MetaDataNode = Select-Xml -XPath "/configuration/appSettings/add[@key='Aos.MetadataDirectory']/@value" -Path $WebConfigFile

    $MetaDataNodeDirectory = $MetaDataNode.Node.Value
    
    Write-PSFMessage -Level Verbose -Message "MetaDataDirectory: $MetaDataNodeDirectory" -Target $MetaDataNodeDirectory

    $AdminFile = "$MetaDataNodeDirectory\Bin\AdminUserProvisioning.exe"

    $TempFileName = New-TemporaryFile
    $TempFileName = $TempFileName.BaseName

    $AdminDll = "$env:TEMP\$TempFileName.dll"

    copy-item -Path $AdminFile -Destination $AdminDll

    $adminAssembly = [System.Reflection.Assembly]::LoadFile($AdminDll)

    $AdminUserUpdater = $adminAssembly.GetType("Microsoft.Dynamics.AdminUserProvisioning.AdminUserUpdater")

    $PublicBinding = [System.Reflection.BindingFlags]::Public
    $StaticBinding = [System.Reflection.BindingFlags]::Static
    $CombinedBinding = $PublicBinding -bor $StaticBinding

    $UpdateAdminUser = $AdminUserUpdater.GetMethod("UpdateAdminUser", $CombinedBinding)
    
    Write-PSFMessage -Level Verbose -Message "Updating Admin using the values $SignInName, $DatabaseServer, $DatabaseName, $SqlUser, $SqlPwd"
    $params = $SignInName, $null, $null, $DatabaseServer, $DatabaseName, $SqlUser, $SqlPwd

    $UpdateAdminUser.Invoke($null, $params)
}


<#
    .SYNOPSIS
        Change the different Azure SQL Database details
         
    .DESCRIPTION
        When preparing an Azure SQL Database to be the new database for an Tier 2+ environment you need to set different details
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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 AxDeployExtUserPwd
        Password obtained from LCS
         
    .PARAMETER AxDbAdminPwd
        Password obtained from LCS
         
    .PARAMETER AxRuntimeUserPwd
        Password obtained from LCS
         
    .PARAMETER AxMrRuntimeUserPwd
        Password obtained from LCS
         
    .PARAMETER AxRetailRuntimeUserPwd
        Password obtained from LCS
         
    .PARAMETER AxRetailDataSyncUserPwd
        Password obtained from LCS
         
    .PARAMETER TenantId
        The ID of tenant that the Azure SQL Database instance is going to be run under
         
    .PARAMETER PlanId
        The ID of the type of plan that the Azure SQL Database is going to be using
         
    .PARAMETER PlanCapability
        The capabilities that the Azure SQL Database instance will be running with
         
    .EXAMPLE
        PS C:\> Set-AzureBacpacValues -DatabaseServer dbserver1.database.windows.net -DatabaseName Import -SqlUser User123 -SqlPwd "Password123" -AxDeployExtUserPwd "Password123" -AxDbAdminPwd "Password123" -AxRuntimeUserPwd "Password123" -AxMrRuntimeUserPwd "Password123" -AxRetailRuntimeUserPwd "Password123" -AxRetailDataSyncUserPwd "Password123" -TenantId "TenantIdFromAzure" -PlanId "PlanIdFromAzure" -PlanCapability "Capabilities"
         
        This will set all the needed details inside the "Import" database that is located in the "dbserver1.database.windows.net" Azure SQL Database instance.
        All service accounts and their passwords will be updated accordingly.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Set-AzureBacpacValues {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DatabaseServer,

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

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

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

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

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

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

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

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

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

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

        [Parameter(Mandatory = $true)]
        [string]$PlanId,
        
        [Parameter(Mandatory = $true)]
        [string]$PlanCapability
    )
        
    $sqlCommand = Get-SQLCommand -DatabaseServer $DatabaseServer -DatabaseName $DatabaseName -SqlUser $SqlUser -SqlPwd $SqlPwd -TrustedConnection $false

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

    $commandText = $commandText.Replace('@axdeployextuser', $AxDeployExtUserPwd)
    $commandText = $commandText.Replace('@axdbadmin', $AxDbAdminPwd)
    $commandText = $commandText.Replace('@axruntimeuser', $AxRuntimeUserPwd)
    $commandText = $commandText.Replace('@axmrruntimeuser', $AxMrRuntimeUserPwd)
    $commandText = $commandText.Replace('@axretailruntimeuser', $AxRetailRuntimeUserPwd)
    $commandText = $commandText.Replace('@axretaildatasyncuser', $AxRetailDataSyncUserPwd)

    $sqlCommand.CommandText = $commandText

    $null = $sqlCommand.Parameters.Add("@TenantId", $TenantId)
    $null = $sqlCommand.Parameters.Add("@PlanId", $PlanId)
    $null = $sqlCommand.Parameters.Add("@PlanCapability ", $PlanCapability)

    try {
        $sqlCommand.Connection.Open()

        Write-PSFMessage -Level Verbose "Execution sql statement against database" -Target $sqlCommand.CommandText
        $null = $sqlCommand.ExecuteNonQuery()
        
        $true
    }
    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()
    }
}


<#
    .SYNOPSIS
        Set the SQL Server specific values
         
    .DESCRIPTION
        Set the SQL Server specific values when restoring a bacpac file
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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:\> Set-SqlBacpacValues -DatabaseServer localhost -DatabaseName "AxDB" -SqlUser "User123" -SqlPwd "Password123"
         
        This will connect to the "AXDB" database that is available in the SQL Server instance running on the localhost.
        It will use the "User123" SQL Server credentials to connect to the SQL Server instance.
        This will set all the necessary SQL Server database options and create the needed objects in side the "AxDB" database.
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Set-SqlBacpacValues {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    [OutputType('System.Boolean')]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DatabaseServer,

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

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

        [Parameter(Mandatory = $false)]
        [string] $SqlPwd,
        
        [Parameter(Mandatory = $false)]
        [bool] $TrustedConnection
    )
    
    $Params = @{DatabaseServer = $DatabaseServer; DatabaseName = $DatabaseName;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd; TrustedConnection = $TrustedConnection;
    }

    $sqlCommand = Get-SQLCommand @Params

    $commandText = (Get-Content "$script:ModuleRoot\internal\sql\set-bacpacvaluessql.sql") -join [Environment]::NewLine
    $commandText = $commandText.Replace('@DATABASENAME', $DatabaseName)

    $sqlCommand.CommandText = $commandText

    try {
        Write-PSFMessage -Level Verbose "Execution sql statement against database" -Target $sqlCommand.CommandText
        $sqlCommand.Connection.Open()
        $sqlCommand.ExecuteNonQuery()

        $true
    }
    catch {
        Write-PSFMessage -Level Critical -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()
    }
}


<#
    .SYNOPSIS
        Test to see if a given user ID exists
         
    .DESCRIPTION
        Test to see if a given user ID exists in the Dynamics 365 for Finance & Operations instance
         
    .PARAMETER SqlCommand
        The SQL Command object that should be used when testing the user ID
         
    .PARAMETER Id
        Id of the user that you want to test exists or not
         
    .EXAMPLE
        PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123"
        PS C:\> Test-AadUserIdInD365FO -SqlCommand $SqlCommand -Id "TestUser"
         
        This will get a SqlCommand object that will connect to the localhost server and the AXDB database, with the sql credential "User123".
        It will query the the database for any user with the Id "TestUser".
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>


function Test-AadUserIdInD365FO {
    
    param (
        [string] $SqlCommand,
        [string] $Id
    )

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

    $null = $sqlCommand.Parameters.Add("@Id", $Id)
      
    $NumFound = $sqlCommand.ExecuteScalar()
    
    Write-PSFMessage -Level Verbose -Message  "Number of user rows found in database $NumFound" -Target $NumFound
    $SqlCommand.Parameters.Clear()

    $NumFound -ne 0
}


<#
    .SYNOPSIS
        Test to see if a given user already exists
         
    .DESCRIPTION
        Test to see if a given user already exists in the Dynamics 365 for Finance & Operations instance
         
    .PARAMETER SqlCommand
        The SQL Command object that should be used when testing the user
         
    .PARAMETER SignInName
        The sign in name (email address) for the user that you want test
         
    .EXAMPLE
        PS C:\> $SqlCommand = Get-SqlCommand -DatabaseServer localhost -DatabaseName AxDB -SqlUser User123 -SqlPwd "Password123"
        PS C:\> Test-AadUserInD365FO -SqlCommand $SqlCommand -SignInName "Claire@contoso.com"
         
        This will get a SqlCommand object that will connect to the localhost server and the AXDB database, with the sql credential "User123".
        It will query the the database for the user with the e-mail address "Claire@contoso.com".
         
    .NOTES
        Author: Rasmus Andersen (@ITRasmus)
        Author: M�tz Jensen (@Splaxi)
         
#>

function Test-AadUserInD365FO {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Data.SqlClient.SqlCommand] $SqlCommand,

        [Parameter(Mandatory = $true)]
        [string] $SignInName
    )
        
    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\test-aaduserind365fo.sql") -join [Environment]::NewLine

    $null = $sqlCommand.Parameters.Add("@Email", $SignInName)
    
    try {
        $NumFound = $sqlCommand.ExecuteScalar()

        Write-PSFMessage -Level Verbose -Message "Number of user rows found in database $NumFound" -Target $NumFound
    }
    catch {
        Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception
        Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1
        return
    }
    finally {
        $SqlCommand.Parameters.Clear()
    }

    $NumFound -ne 0
}


<#
    .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
        The 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
        Switch to instruct the cmdlet to create the directory if it doesn't exist
         
    .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 )]
        [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
            $null = 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
        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("ImportModeTier2")) -or ($Inputs.ContainsKey("ExportModeTier2"))){
        Write-PSFMessage -Level Verbose -Message "Not capable of using Trusted Connection based on Tier validation."
        $false
    }
    elseif (($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 {
        Write-PSFMessage -Level Verbose -Message "Capabilities based on the centralized logic in the psm1 file." -Target $Script:CanUseTrustedConnection
        $Script:CanUseTrustedConnection
    }
}


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

function Update-TopologyFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )
    
    $topologyFile = Join-Path $Path 'DefaultTopologyData.xml'
                
    Write-PSFMessage -Level Verbose "Creating topology file: $topologyFile"
                
    [xml]$xml = Get-Content $topologyFile
    $machine = $xml.TopologyData.MachineList.Machine
    $machine.Name = $env:computername
                
    $serviceModelList = $machine.ServiceModelList
    $serviceModelList.RemoveAll()
 
    $instalInfoDll = Join-Path $Path 'Microsoft.Dynamics.AX.AXInstallationInfo.dll'
    [void][System.Reflection.Assembly]::LoadFile($instalInfoDll)
 
    $models = [Microsoft.Dynamics.AX.AXInstallationInfo.AXInstallationInfo]::GetInstalledServiceModel()

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


<#
    .SYNOPSIS
        Save an Azure Storage Account config
         
    .DESCRIPTION
        Adds an Azure Storage Account config to the configuration store
         
    .PARAMETER Name
        The logical name of the Azure Storage Account you are about to registered in the configuration store
         
    .PARAMETER AccountId
        The account id for the Azure Storage Account you want to register in the configuration store
         
    .PARAMETER AccessToken
        The access token for the Azure Storage Account you want to register in the configuration store
         
    .PARAMETER Blobname
        The name of the blob inside the Azure Storage Account you want to register in the configuration store
         
    .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 Force
        Switch to instruct the cmdlet to overwrite already registered Azure Storage Account entry
         
    .EXAMPLE
        PS C:\> Add-D365AzureStorageConfig -Name "UAT-Exports" -AccountId "1234" -AccessToken "dafdfasdfasdf" -Blob "testblob"
         
        This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", AccessToken "dafdfasdfasdf" and Blob "testblob".
         
    .EXAMPLE
        PS C:\> Add-D365AzureStorageConfig -Name "UAT-Exports" -AccountId "1234" -AccessToken "dafdfasdfasdf" -Blob "testblob" -ConfigStorageLocation "System"
         
        This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", AccessToken "dafdfasdfasdf" and Blob "testblob".
        All configuration objects will be persisted in the system wide configuration store.
        This will enable all users to access the configuration objects and their values.
         
    .NOTES
         
        You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working.
         
        Author: M�tz Jensen (@Splaxi)
         
#>

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

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

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

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

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

        [switch] $Force
    )

    $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation

    if (Test-PSFFunctionInterrupt) { return }

    
    $Details = @{AccountId = $AccountId; AccessToken = $AccessToken;
        Blobname = $Blobname;
    }

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

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

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

        Set-PSFConfig -FullName "d365fo.tools.azure.storage.accounts" -Value $Accounts
        Register-PSFConfig -FullName "d365fo.tools.azure.storage.accounts" -Scope $configScope
    }
}


<#
    .SYNOPSIS
        Save an environment config
         
    .DESCRIPTION
        Adds an environment config to the configuration store
         
    .PARAMETER Name
        The logical name of the environment you are about to registered in the configuration
         
    .PARAMETER URL
        The URL to the environment you want the module to use when possible
         
    .PARAMETER SqlUser
        The login name for the SQL Server instance
         
    .PARAMETER SqlPwd
        The password for the SQL Server user
         
    .PARAMETER Company
        The company you want to work against when calling any browser based cmdlets
         
        The default value is "DAT"
    .PARAMETER TfsUri
        The URI for the TFS / VSTS account that you are working against.
         
    .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 Force
        Switch to instruct the cmdlet to overwrite already registered environment entry
         
    .EXAMPLE
        PS C:\> Add-D365EnvironmentConfig -Name "Customer-UAT" -URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF" -Company "DAT"
         
        This will add an entry into the list of environments that is stored with the name "Customer-UAT" and with the URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF".
        The company is registered "DAT".
         
    .EXAMPLE
        PS C:\> Add-D365EnvironmentConfig -Name "Customer-UAT" -URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF" -Company "DAT" -SqlUser "SqlAdmin" -SqlPwd "Pass@word1"
         
        This will add an entry into the list of environments that is stored with the name "Customer-UAT" and with the URL "https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF".
        It will register the SqlUser as "SqlAdmin" and the SqlPassword to "Pass@word1".
         
        This it useful for working on Tier 2 environments where the SqlUser and SqlPassword cannot be extracted from the environment itself.
         
    .NOTES
         
        You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working.
         
#>

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

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

        [string] $SqlUser = "sqladmin",

        [string] $SqlPwd,

        [string] $Company = "DAT",

        [string] $TfsUri,

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

        [switch] $Force
    )

    $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation

    if (Test-PSFFunctionInterrupt) { return }

    $Details = @{URL = $URL; Company = $Company;
        SqlUser = $SqlUser; SqlPwd = $SqlPwd;
        TfsUri = $TfsUri;
    }

    $Environments = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.environments")

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

            Set-PSFConfig -FullName "d365fo.tools.environments" -Value $Environments
            Register-PSFConfig -FullName "d365fo.tools.environments" -Scope $configScope
        }
        else {
            Write-PSFMessage -Level Host -Message "An environment with that name <c='em'>already exists</c>. You want to <c='em'>overwrite</c> the already registered details please supply the <c='em'>-Force</c> parameter."
            Stop-PSFFunction -Message "Stopping because an environment already exists with that name."
            return
        }
    }
    else {
        $null = $Environments.Add($Name, $Details)

        Set-PSFConfig -FullName "d365fo.tools.environments" -Value $Environments
        Register-PSFConfig -FullName "d365fo.tools.environments" -Scope $configScope
    }
}


<#
    .SYNOPSIS
        Create a backup of the Metadata directory
         
    .DESCRIPTION
        Creates a backup of all the files and folders from the Metadata directory
         
    .PARAMETER MetaDataDir
        Path to the Metadata directory
         
        Default value is the PackagesLocalDirectory
         
    .PARAMETER BackupDir
        Path where you want the backup to be place
         
    .EXAMPLE
        PS C:\> Backup-D365MetaDataDir
         
        This will backup the PackagesLocalDirectory and create an PackagesLocalDirectory_backup next to it
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Backup-D365MetaDataDir {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [string] $MetaDataDir = "$Script:MetaDataDir",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [string] $BackupDir = "$($Script:MetaDataDir)_backup"
        
    )

    if(!(Test-Path -Path $MetaDataDir -Type Container)) {
        Write-PSFMessage -Level Host -Message "The <c='em'>$MetaDataDir</c> path wasn't found. Please ensure the path <c='em'>exists </c> and you have enough <c='em'>permission/c> to access the directory."
        Stop-PSFFunction -Message "Stopping because the path is missing."
        return
    }

    Invoke-TimeSignal -Start

    $Params = @($MetaDataDir, $BackupDir, "/MT:4", "/E", "/NFL",
    "/NDL", "/NJH", "/NC", "/NS", "/NP")

    Start-Process -FilePath "Robocopy.exe" -ArgumentList $Params -NoNewWindow -Wait

    Invoke-TimeSignal -End
}


<#
    .SYNOPSIS
        Clear the monitoring data from a Dynamics 365 for Finance & Operations machine
         
    .DESCRIPTION
        Clear the monitoring data that is filling up the service drive on a Dynamics 365 for Finance & Operations
         
    .PARAMETER Path
        The path to where the monitoring data is located
         
        The default value is the "ServiceDrive" (j:\ | k:\) and the \MonAgentData\SingleAgent\Tables folder structure
         
    .EXAMPLE
        PS C:\> Clear-D365MonitorData
         
        This will delete all the files that are located in the default path on the machine.
        Some files might be locked by a process, but the cmdlet will attemp to delete all files.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Clear-D365MonitorData {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Position = 1, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)]
        [string] $Path = (Join-Path ([System.Environment]::ExpandEnvironmentVariables("%ServiceDrive%")) "\MonAgentData\SingleAgent\Tables")
    )
    
    Get-ChildItem -Path $Path | Remove-Item -Force -ErrorAction SilentlyContinue
}


<#
    .SYNOPSIS
        Sets the environment back into operating state
         
    .DESCRIPTION
        Sets the Dynamics 365 environment back into operating / running state after been in maintenance mode
         
    .PARAMETER MetaDataDir
        The path to the meta data directory for the environment
         
        Default path is the same as the aos service PackagesLocalDirectory
         
    .PARAMETER BinDir
        The path to the bin directory for the environment
         
        Default path is the same as the aos service PackagesLocalDirectory\bin
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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.
         
    .EXAMPLE
        PS C:\> Disable-D365MaintenanceMode
         
        This will execute the Microsoft.Dynamics.AX.Deployment.Setup.exe with the default values that was pulled from the environment and put the environment into the operate / running state.
         
    .NOTES
        Author: M�tz Jensen (@splaxi)
        Author: Tommy Skaue (@skaue)
         
        The cmdlet wraps the execution of Microsoft.Dynamics.AX.Deployment.Setup.exe and parses the parameters needed
         
#>

function Disable-D365MaintenanceMode {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [string] $MetaDataDir = "$Script:MetaDataDir",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [string] $BinDir = "$Script:BinDir",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )]
        [string] $DatabaseServer = $Script:DatabaseServer,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )]
        [string] $DatabaseName = $Script:DatabaseName,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 5 )]
        [string] $SqlUser = $Script:DatabaseUserName,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 6 )]
        [string] $SqlPwd = $Script:DatabaseUserPassword
    )
    
    if ((Get-Process -Name "devenv" -ErrorAction SilentlyContinue).Count -gt 0) {
        Write-PSFMessage -Level Host -Message "It seems that you have a <c='em'>Visual Studio</c> running. Please <c='em'>exit</c> Visual Studio and run the cmdlet again."
        Stop-PSFFunction -Message "Stopping because of running Visual Studio."
        return
    }

    if(-not ($Script:IsAdminRuntime)) {
        
        Write-PSFMessage -Level Verbose -Message "Setting Maintenance Mode without using executable (requires local admin)."
        
        Stop-D365Environment -All
        
        $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters

        $Params = @{
            DatabaseServer = $DatabaseServer
            DatabaseName   = $DatabaseName
            SqlUser        = $SqlUser
            SqlPwd         = $SqlPwd
        }

        Invoke-D365SqlScript @Params -FilePath $("$script:PSModuleRoot\internal\sql\disable-maintenancemode.sql") -TrustedConnection $UseTrustedConnection

        Start-D365Environment -All
    }
    else {

        $executable = Join-Path $BinDir "bin\Microsoft.Dynamics.AX.Deployment.Setup.exe"

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

        $params = @("-isemulated", "true",
            "-sqluser", "$SqlUser",
            "-sqlpwd", "$SqlPwd",
            "-sqlserver", "$DatabaseServer",
            "-sqldatabase", "$DatabaseName",
            "-metadatadir", "$MetaDataDir",
            "-bindir", "$BinDir",
            "-setupmode", "maintenancemode",
            "-isinmaintenancemode", "false")

        Stop-D365Environment -All

        Start-Process -FilePath $executable -ArgumentList ($params -join " ") -NoNewWindow -Wait

        Start-D365Environment -All
    }
}


<#
    .SYNOPSIS
        Disables the user in D365FO
         
    .DESCRIPTION
        Sets the enabled to 0 in the userinfo table.
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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 Email
        The search string to select which user(s) should be disabled.
         
        The parameter supports wildcards. E.g. -Email "*@contoso.com*"
         
    .EXAMPLE
        PS C:\> Disable-D365User
         
        This will Disable all users for the environment
         
    .EXAMPLE
        PS C:\> Disable-D365User -Email "claire@contoso.com"
         
        This will Disable the user with the email address "claire@contoso.com"
         
    .EXAMPLE
        PS C:\> Disable-D365User -Email "*contoso.com"
         
        This will Disable all users that matches the search "*contoso.com" in their email address
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Disable-D365User {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$DatabaseServer = $Script:DatabaseServer,

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

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

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

        [Parameter(Mandatory = $false, Position = 5)]
        [string]$Email

    )

    $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters

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

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

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\disable-user.sql") -join [Environment]::NewLine
    
    $null = $sqlCommand.Parameters.AddWithValue('@Email', $Email.Replace("*", "%"))

    try {
        Write-PSFMessage -Level Verbose -Message "Executing the update statement against the database."
        $sqlCommand.Connection.Open()

        $reader = $sqlCommand.ExecuteReader()

        while ($reader.Read() -eq $true) {
            Write-PSFMessage -Level Verbose -Message "User $($reader.GetString(0)), $($reader.GetString(1)), $($reader.GetString(2)) Updated"
        }

        $reader.Close()
        $NumAffected = $reader.RecordsAffected
        Write-PSFMessage -Level Verbose -Message "Users updated : $NumAffected"
    }
    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 {
        $reader.close()

        if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
            $sqlCommand.Connection.Close()
        }

        $sqlCommand.Dispose()
    }
}


<#
    .SYNOPSIS
        Sets the environment into maintenance mode
         
    .DESCRIPTION
        Sets the Dynamics 365 environment into maintenance mode to enable the user to update the license configuration
         
    .PARAMETER MetaDataDir
        The path to the meta data directory for the environment
         
        Default path is the same as the aos service PackagesLocalDirectory
         
    .PARAMETER BinDir
        The path to the bin directory for the environment
         
        Default path is the same as the aos service PackagesLocalDirectory\bin
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN).
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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.
         
    .EXAMPLE
        PS C:\> Enable-D365MaintenanceMode
         
        This will execute the Microsoft.Dynamics.AX.Deployment.Setup.exe with the default values that was pulled from the environment and put the environment into the operate / running state
         
    .NOTES
        Author: M�tz Jensen (@splaxi)
        Author: Tommy Skaue (@skaue)
         
        The cmdlet wraps the execution of Microsoft.Dynamics.AX.Deployment.Setup.exe and parses the parameters needed
         
#>

function Enable-D365MaintenanceMode {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [string] $MetaDataDir = "$Script:MetaDataDir",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [string] $BinDir = "$Script:BinDir",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )]
        [string] $DatabaseServer = $Script:DatabaseServer,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 4 )]
        [string] $DatabaseName = $Script:DatabaseName,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 5 )]
        [string] $SqlUser = $Script:DatabaseUserName,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 6 )]
        [string] $SqlPwd = $Script:DatabaseUserPassword
    )

    if ((Get-Process -Name "devenv" -ErrorAction SilentlyContinue).Count -gt 0) {
        Write-PSFMessage -Level Host -Message "It seems that you have a <c='em'>Visual Studio</c> running. Please <c='em'>exit</c> Visual Studio and run the cmdlet again."
        Stop-PSFFunction -Message "Stopping because of running Visual Studio."
        return
    }
    
    if(-not ($Script:IsAdminRuntime)) {
        
        Write-PSFMessage -Level Verbose -Message "Setting Maintenance Mode without using executable (requires local admin)."
        
        Stop-D365Environment -All
        
        $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters

        $Params = @{
            DatabaseServer = $DatabaseServer
            DatabaseName   = $DatabaseName
            SqlUser        = $SqlUser
            SqlPwd         = $SqlPwd
        }

        Invoke-D365SqlScript @Params -FilePath $("$script:PSModuleRoot\internal\sql\enable-maintenancemode.sql") -TrustedConnection $UseTrustedConnection

        Start-D365Environment -Aos
    }
    else {

        $executable = Join-Path $BinDir "bin\Microsoft.Dynamics.AX.Deployment.Setup.exe"

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

        $params = @("-isemulated", "true",
            "-sqluser", "$SqlUser",
            "-sqlpwd", "$SqlPwd",
            "-sqlserver", "$DatabaseServer",
            "-sqldatabase", "$DatabaseName",
            "-metadatadir", "$MetaDataDir",
            "-bindir", "$BinDir",
            "-setupmode", "maintenancemode",
            "-isinmaintenancemode", "true")

        Stop-D365Environment -All

        Start-Process -FilePath $executable -ArgumentList ($params -join " ") -NoNewWindow -Wait

        Start-D365Environment -Aos
    }
}


<#
    .SYNOPSIS
        Enables the user in D365FO
         
    .DESCRIPTION
        Sets the enabled to 1 in the userinfo table
         
    .PARAMETER DatabaseServer
        The name of the database server
         
        If on-premises or classic SQL Server, use either short name og Fully Qualified Domain Name (FQDN)
         
        If Azure use the full address to the database server, e.g. server.database.windows.net
         
    .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 Email
        The search string to select which user(s) should be enabled
         
        The parameter supports wildcards. E.g. -Email "*@contoso.com*"
         
        Default value is "*" to update all users
         
    .EXAMPLE
        PS C:\> Enable-D365User
         
        This will enable all users for the environment
         
    .EXAMPLE
        PS C:\> Enable-D365User -Email "claire@contoso.com"
         
        This will enable the user with the email address "claire@contoso.com"
         
    .EXAMPLE
        PS C:\> Enable-D365User -Email "*contoso.com"
         
        This will enable all users that matches the search "*contoso.com" in their email address
         
    .NOTES
        Implemented on request by Paul Heisterkamp
         
        Author: M�tz Jensen
         
#>

function Enable-D365User {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$DatabaseServer = $Script:DatabaseServer,

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

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

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

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

    )

    $UseTrustedConnection = Test-TrustedConnection $PSBoundParameters

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

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

    $sqlCommand.CommandText = (Get-Content "$script:ModuleRoot\internal\sql\enable-user.sql") -join [Environment]::NewLine
    
    $null = $sqlCommand.Parameters.AddWithValue('@Email', $Email.Replace("*", "%"))

    try {
        Write-PSFMessage -Level Verbose -Message "Executing the update statement against the database."
        $sqlCommand.Connection.Open()

        $reader = $sqlCommand.ExecuteReader()

        while ($reader.Read() -eq $true) {
            Write-PSFMessage -Level Verbose -Message "User $($reader.GetString(0)), $($reader.GetString(1)), $($reader.GetString(2)) Updated"
        }

        $reader.Close()
        $NumAffected = $reader.RecordsAffected
        Write-PSFMessage -Level Verbose -Message "Users updated : $NumAffected"
    }
    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 {
        $reader.close()

        if ($sqlCommand.Connection.State -ne [System.Data.ConnectionState]::Closed) {
            $sqlCommand.Connection.Close()
        }

        $sqlCommand.Dispose()
    }
}


<#
    .SYNOPSIS
        Extract details from a User Interface Security file
         
    .DESCRIPTION
        Extracts and partitions the security details from an User Interface Security file into the same structure as AOT security files
         
    .PARAMETER FilePath
        Path to the User Interface Security XML file you want to work against
         
    .PARAMETER OutputDirectory
        Path to the folder where the cmdlet will output and structure the details from the file.
        The cmdlet will create a sub folder named like the input file.
         
        Default value is: "C:\temp\d365fo.tools\security-extraction"
         
    .EXAMPLE
        PS C:\> Export-D365SecurityDetails -FilePath C:\temp\d365fo.tools\SecurityDatabaseCustomizations.xml
         
        This will grab all the details inside the "C:\temp\d365fo.tools\SecurityDatabaseCustomizations.xml" file and extract that into the default path "C:\temp\d365fo.tools\security-extraction"
         
    .NOTES
        Author: M�tz Jensen (@splaxi)
         
        The work and design of this cmdlet is based on the findings by Alex Meyer (@alexmeyer_ITGuy).
         
        He wrote about his findings on his blog:
        https://alexdmeyer.com/2018/09/26/converting-d365fo-user-interface-security-customizations-export-to-aot-security-xml-files/
         
        He published a github repository:
         
        https://github.com/ameyer505/D365FOSecurityConverter
         
        All credits goes to Alex Meyer
#>

function Export-D365SecurityDetails {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Alias('Path')]
        [string]$FilePath,

        [Parameter(Mandatory = $false)]
        [Alias('Output')]
        [string]$OutputDirectory = "C:\temp\d365fo.tools\security-extraction"
    )
    
    begin { }
    
    process {

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

        [xml] $xdoc = Get-Content $FilePath
        
        $fileName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath)
        
        $OutputDirectory = Join-Path $OutputDirectory $fileName

        Write-PSFMessage -Level Verbose -Message "Creating the output directory for the extraction" -Target $OutputDirectory
        $null = New-Item -Path $OutputDirectory -ItemType Directory -Force -ErrorAction SilentlyContinue

        Write-PSFMessage -Level Verbose -Message "Getting all the security objects."
        $secObjects = $xdoc.SelectNodes("/*/*/*/*/*[starts-with(name(),'AxSec')]")

        if ($secObjects.Count -gt 0) {

            Write-PSFMessage -Level Verbose -Message "Looping through all the security objects we found"
            foreach ( $secObject in $secObjects) {
                
                $secPath = Join-Path $OutputDirectory $secObject.LocalName
                
                $null = New-Item -Path $secPath -ItemType Directory -Force -ErrorAction SilentlyContinue

                $secObjectName = $secObject.Name
                
                if (-not ([string]::IsNullOrEmpty($secObjectName))) {
                    $filePathOut = Join-Path $secPath $secObjectName
                    $filePathOut += ".xml"

                    Write-PSFMessage -Level Verbose -Message "Generating the output file: $filePathOut" -Target $filePathOut
                    $secObject.OuterXml | Out-File $filePathOut
                }
            }
        }
    }
    
    end {
    }
}

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

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


<#
    .SYNOPSIS
        Get active Azure Storage Account configuration
         
    .DESCRIPTION
        Get active Azure Storage Account configuration object from the configuration store
         
    .EXAMPLE
        PS C:\> Get-D365ActiveAzureStorageConfig
         
        This will get the active Azure Storage configuration
         
    .NOTES
        You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working.
         
        Author: M�tz Jensen (@Splaxi)
         
#>

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

    Get-PSFConfigValue -FullName "d365fo.tools.active.azure.storage.account"
}


<#
    .SYNOPSIS
        Get active environment configuration
         
    .DESCRIPTION
        Get active environment configuration object from the configuration store
         
    .EXAMPLE
        PS C:\> Get-D365ActiveEnvironmentConfig
         
        This will get the active environment configuration
         
    .NOTES
        You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working.
         
        Author: M�tz Jensen (@Splaxi)
         
#>

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

    (Get-PSFConfigValue -FullName "d365fo.tools.active.environment")
}


<#
    .SYNOPSIS
        Search for AOT object
         
    .DESCRIPTION
        Enables you to search for different AOT objects
         
    .PARAMETER Path
        Path to the package that you want to work against
         
    .PARAMETER ObjectType
        The type of AOT object you're searching for
         
    .PARAMETER Name
        Name of the object that you're looking for
         
        Accepts wildcards for searching. E.g. -Name "Work*status"
         
        Default value is "*" which will search for all objects
         
    .PARAMETER SearchInPackages
        Switch to instruct the cmdlet to search in packages directly instead
        of searching in the XppMetaData directory under a given package
         
    .PARAMETER IncludePath
        Switch to instruct the cmdlet to include the path for the object found
         
    .EXAMPLE
        PS C:\> Get-D365AOTObject -Name *flush* -ObjectType AxClass -Path "C:\AOSService\PackagesLocalDirectory\ApplicationFoundation"
         
        This will search inside the ApplicationFoundation package for all AxClasses that matches the search *flush*.
         
    .EXAMPLE
        PS C:\> Get-D365AOTObject -Name *flush* -ObjectType AxClass -IncludePath -Path "C:\AOSService\PackagesLocalDirectory\ApplicationFoundation"
         
        This will search inside the ApplicationFoundation package for all AxClasses that matches the search *flush* and include the full path to the files.
         
    .EXAMPLE
        PS C:\> Get-D365InstalledPackage -Name Application* | Get-D365AOTObject -Name *flush* -ObjectType AxClass
         
        This searches for all packages that matches Application* and pipes them into Get-D365AOTObject which will search for all AxClasses that matches the search *flush*.
         
    .EXAMPLE
        This is an advanced example and shouldn't be something you resolve to every time.
         
        PS C:\> Get-D365AOTObject -Path "C:\AOSService\PackagesLocalDirectory\*" -Name *flush* -ObjectType AxClass -SearchInPackages
         
        This will search across all packages and will look for the all AxClasses that matches the search *flush*.
        It will NOT search in the XppMetaData directory for each package.
         
        This can stress your system.
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-D365AOTObject {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 1)]
        [Alias('PackageDirectory')]
        [string] $Path,

        [Parameter(Mandatory = $false, Position = 2)]
        [ValidateSet('AxAggregateDataEntity', 'AxClass', 'AxCompositeDataEntityView',
            'AxDataEntityView', 'AxForm', 'AxMap', 'AxQuery', 'AxTable', 'AxView')]
        [Alias('Type')]
        [string[]] $ObjectType = @("AxClass"),

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

        [Parameter(Mandatory = $false, Position = 4)]
        [switch] $SearchInPackages,

        [Parameter(Mandatory = $false, Position = 5)]
        [switch] $IncludePath
    )
    
    begin {
        
    }
    
    process {
        $SearchList = New-Object -TypeName "System.Collections.ArrayList"

        foreach ($item in $ObjectType) {
            if ($SearchInPackages.IsPresent) {
                $SearchParent = Split-Path $Path -Leaf

                $null = $SearchList.Add((Join-Path "$Path" "\$SearchParent\$item\*.xml"))
                $SearchParent = $item #* Hack to make the logic when selecting the output work as expected
            }
            else {
                $SearchParent = "XppMetadata"

                $null = $SearchList.Add((Join-Path "$Path" "\$SearchParent\*\$item\*.xml"))
            }
        }
        
        #* We are searching files - so the last character has to be a *
        if($Name.Substring($Name.Length -1, 1) -ne "*") {$Name = "$Name*"}

        $Files = Get-ChildItem -Path ($SearchList.ToArray()) -Filter $Name

        if($IncludePath.IsPresent) {
            $Files | Select-PSFObject -TypeName "D365FO.TOOLS.AotObject" "BaseName as Name",
            @{Name = "AotType"; Expression = {Split-Path(Split-Path -Path $_.Fullname -Parent) -leaf }},
            @{Name = "Model"; Expression = {Split-Path(($_.Fullname -Split $SearchParent)[0] ) -leaf }},
            "Fullname as Path"
        }
        else {
            $Files | Select-PSFObject -TypeName "D365FO.TOOLS.AotObject" "BaseName as Name",
            @{Name = "AotType"; Expression = {Split-Path(Split-Path -Path $_.Fullname -Parent) -leaf }},
            @{Name = "Model"; Expression = {Split-Path(($_.Fullname -Split $SearchParent)[0] ) -leaf }}
        }
    }
    
    end {
    }
}


<#
    .SYNOPSIS
        Get Azure Storage Account configs
         
    .DESCRIPTION
        Get all Azure Storage Account configuration objects from the configuration store
         
    .PARAMETER Name
        The name of the Azure Storage Account you are looking for
         
        Default value is "*" to display all Azure Storage Account configs
         
    .EXAMPLE
        PS C:\> Get-D365AzureStorageConfig
         
        This will show all Azure Storage Account configs
         
    .NOTES
        You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working.
         
        Author: M�tz Jensen (@Splaxi)
         
#>

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

    )
    
    $Environments = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.azure.storage.accounts")
        
    foreach ($item in $Environments.Keys) {
        if ($item -NotLike $Name) { continue }
        $temp = [ordered]@{Name = $item}
        $temp += $Environments[$item]
        [PSCustomObject]$temp
    }
}


<#
    .SYNOPSIS
        Get a file from Azure
         
    .DESCRIPTION
        Get all files from an Azure Storage Account
         
    .PARAMETER AccountId
        Storage Account Name / Storage Account Id where you want to look for files
         
    .PARAMETER AccessToken
        The token that has the needed permissions for the search action
         
    .PARAMETER Blobname
        Name of the container / blog inside the storage account you want to look for files
         
    .PARAMETER Name
        Name of the file you are looking for
         
        Accepts wildcards for searching. E.g. -Name "Application*Adaptor"
         
        Default value is "*" which will search for all packages
         
    .PARAMETER GetLatest
        Switch to instruct the cmdlet to only fetch the latest file from the Azure Storage Account
         
    .EXAMPLE
        PS C:\> Get-D365AzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Blobname "backupfiles"
         
        Will get all files in the blob / container
         
    .EXAMPLE
        PS C:\> Get-D365AzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Blobname "backupfiles" -Name "*UAT*"
         
        Will get all files in the blob / container that fits the "*UAT*" search value
         
    .NOTES
         
#>

function Get-D365AzureStorageFile {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false, Position = 1 )]
        [string] $AccountId = $Script:AccountId,

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

        [Parameter(Mandatory = $false, Position = 3 )]
        [string] $Blobname = $Script:Blobname,

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

        [switch] $GetLatest
    )

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


    PROCESS {
        if (Test-PSFFunctionInterrupt) { return }

        $storageContext = new-AzureStorageContext -StorageAccountName $AccountId -StorageAccountKey $AccessToken

        $cloudStorageAccount = [Microsoft.WindowsAzure.Storage.CloudStorageAccount]::Parse($storageContext.ConnectionString)

        $blobClient = $cloudStorageAccount.CreateCloudBlobClient()

        $blobcontainer = $blobClient.GetContainerReference($Blobname);

        try {
            $files = $blobcontainer.ListBlobs() | Sort-Object -Descending { $_.Properties.LastModified }

            if ($GetLatest) {
                $files | Select-Object -First 1
            }
            else {
    
                foreach ($obj in $files) {
                    if ($obj.Name -NotLike $Name) { continue }

                    $obj
                }
            }
        }
        catch {
            Write-PSFMessage -Level Warning -Message "Something broke" -ErrorRecord $_
        }
    }
    END {}
}


<#
    .SYNOPSIS
        Get the ClickOnce configuration
         
    .DESCRIPTION
        Creates the needed registry keys and values for ClickOnce to work on the machine
         
    .EXAMPLE
        PS C:\> Get-D365ClickOnceTrustPrompt
         
        This will get the current ClickOnce configuration
         
    .NOTES
        General notes
#>

function Get-D365ClickOnceTrustPrompt {
    [CmdletBinding()]
    param (
    
    )
    
    begin {
    }
    
    process {
        Write-PSFMessage -Level Verbose -Message "Testing if the registry key exists or not"

        if ((Test-Path -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel") -eq $false) {
            Write-PSFMessage -Level Host -Message "It looks like ClickOnce trust prompt has never been configured on this machine. Run Set-D365ClickOnceTrustPrompt to fix that"
        }
        else {
            Write-PSFMessage -Level Verbose -Message "Gathering the details from registry"

            [PSCustomObject]@{
                UntrustedSites = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "UntrustedSites").UntrustedSites
                Internet       = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "Internet").Internet
                MyComputer     = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "MyComputer").MyComputer
                LocalIntranet  = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "LocalIntranet").LocalIntranet
                TrustedSites   = (Get-ItemProperty -Path "HKLM:\SOFTWARE\MICROSOFT\.NETFramework\Security\TrustManager\PromptingLevel" "TrustedSites").TrustedSites
            }
        }

    }
    
    end {
    }
}


<#
    .SYNOPSIS
        Shows the Database Access information for the D365 Environment
         
    .DESCRIPTION
        Gets all database information from the D365 environment
         
    .EXAMPLE
        PS C:\> Get-D365DatabaseAccess
         
        This will get all relevant details, including connection details, for the database configured for the environment
         
    .NOTES
        The cmdlet wraps the call against a dll file that is shipped with Dynamics 365 for Finance & Operations.
        The call to the dll file gets all relevant connections details for the database server.
         
        Author: Rasmus Andersen (@ITRasmus)
#>

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

    $environment = Get-ApplicationEnvironment
    
    return $environment.DataAccess
}


<#
    .SYNOPSIS
        Decrypts the AOS config file
         
    .DESCRIPTION
        Function used for decrypting the config file used by the D365 Finance & Operations AOS service
         
    .PARAMETER DropPath
        Place where the decrypted files should be placed
         
    .PARAMETER AosServiceWebRootPath
        Location of the D365 webroot folder
         
    .EXAMPLE
        PS C:\> Get-D365DecryptedConfigFile -DropPath "c:\temp\d365fo.tools"
         
        This will get the config file from the instance, decrypt it and save it to "c:\temp\d365fo.tools"
         
    .NOTES
        Used for getting the Password for the database and other service accounts used in environment
         
        Author : Rasmus Andersen (@ITRasmus)
        Author : M�tz Jensen (@splaxi)
         
#>

function Get-D365DecryptedConfigFile {
    param(
        [Parameter(Mandatory = $false, Position = 1)]
        [Alias('ExtractFolder')]
        [string]$DropPath = "C:\temp\d365fo.tools\ConfigFile_Decrypted",

        [Parameter(Mandatory = $false, Position = 2)]
        [string]$AosServiceWebRootPath = $Script:AOSPath
    )

    $WebConfigFile = Join-Path $AosServiceWebRootPath $Script:WebConfig

    if (!(Test-PathExists -Path $WebConfigFile -Type Leaf)) {return}
    if (!(Test-PathExists -Path $DropPath -Type Container -Create)) {return}

    Write-PSFMessage -Level Verbose -Message "Starting the decryption logic"
    New-DecryptedFile $WebConfigFile $DropPath
}


<#
    .SYNOPSIS
        Get a .NET class from the Dynamics 365 for Finance and Operations installation
         
    .DESCRIPTION
        Get a .NET class from an assembly file (dll) from the package directory
         
    .PARAMETER Name
        Name of the .NET class that you are looking for
         
        Accepts wildcards for searching. E.g. -Name "ER*Excel*"
         
        Default value is "*" which will search for all classes
         
    .PARAMETER Assembly
        Name of the assembly file that you want to search for the .NET class
         
        Accepts wildcards for searching. E.g. -Name "*AX*Framework*.dll"
         
        Default value is "*.dll" which will search for assembly files
         
    .PARAMETER PackageDirectory
        Path to the directory containing the installed packages
         
        Normally it is located under the AOSService directory in "PackagesLocalDirectory"
         
        Default value is fetched from the current configuration on the machine
         
    .EXAMPLE
        PS C:\> Get-D365DotNetClass -Name "ERText*"
         
        Will search across all assembly files (*.dll) that are located in the default package directory after
        any class that fits the search "ERText*"
         
    .EXAMPLE
        PS C:\> Get-D365DotNetClass -Name "ERText*" -Assembly "*LocalizationFrameworkForAx.dll*"
         
        Will search across all assembly files (*.dll) that are fits the search "*LocalizationFrameworkForAx.dll*",
        that are located in the default package directory, after any class that fits the search "ERText*"
         
    .EXAMPLE
        PS C:\> Get-D365DotNetClass -Name "ERText*" | Export-Csv -Path c:\temp\results.txt -Delimiter ";"
         
        Will search across all assembly files (*.dll) that are located in the default package directory after
        any class that fits the search "ERText*"
         
        The output is saved to a file to make it easier to search inside the result set
         
    .NOTES
        The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages.
         
#>

function Get-D365DotNetClass {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [string] $Name = "*",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [string] $Assembly = "*.dll",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )]
        [string] $PackageDirectory = $Script:PackageDirectory
    )
    
    begin {
    }
    
    process {
        Invoke-TimeSignal -Start

        $files = (Get-ChildItem -Path $PackageDirectory -Filter $Assembly -Recurse -Exclude "*Resources*" | Where-Object Fullname -Notlike "*Resources*" )

        $files | ForEach-Object {
            $path = $_.Fullname
            try {
                Write-PSFMessage -Level Verbose -Message "Loading the dll file: $path" -Target $path
                
                [Reflection.Assembly]$ass = [Reflection.Assembly]::LoadFile($path)

                $res = $ass.GetTypes()

                Write-PSFMessage -Level Verbose -Message "Looping through all types from the assembly"
                foreach ($obj in $res) {
                    if ($obj.Name -NotLike $Name) { continue }
                    [PSCustomObject]@{
                        IsPublic = $obj.IsPublic
                        IsSerial = $obj.IsSerial
                        Name     = $obj.Name
                        BaseType = $obj.BaseType
                        File     = $path
                    }
                }
            }
            catch {
                Write-PSFMessage -Level Host -Message "Something went wrong while trying to load the path: $path" -Exception $PSItem.Exception
                Stop-PSFFunction -Message "Stopping because of errors"
                return
            }
        }

        Invoke-TimeSignal -End
    }

    end {
    }

}


<#
    .SYNOPSIS
        Get a .NET method from the Dynamics 365 for Finance and Operations installation
         
    .DESCRIPTION
        Get a .NET method from an assembly file (dll) from the package directory
         
    .PARAMETER Assembly
        Name of the assembly file that you want to search for the .NET method
         
        Provide the full path for the assembly file you want to work against
         
    .PARAMETER Name
        Name of the .NET method that you are looking for
         
        Accepts wildcards for searching. E.g. -Name "parmER*Excel*"
         
        Default value is "*" which will search for all methods
         
    .PARAMETER TypeName
        Name of the .NET class that you want to work against
         
        Accepts wildcards for searching. E.g. -Name "*ER*Excel*"
         
        Default value is "*" which will work against all classes
         
    .EXAMPLE
        PS C:\> Get-D365DotNetMethod -Assembly "C:\AOSService\PackagesLocalDirectory\ElectronicReporting\bin\Microsoft.Dynamics365.LocalizationFrameworkForAx.dll"
         
        Will get all methods, across all classes, from the assembly file
         
    .EXAMPLE
        PS C:\> Get-D365DotNetMethod -Assembly "C:\AOSService\PackagesLocalDirectory\ElectronicReporting\bin\Microsoft.Dynamics365.LocalizationFrameworkForAx.dll" -TypeName "ERTextFormatExcelFileComponent"
         
        Will get all methods, from the "ERTextFormatExcelFileComponent" class, from the assembly file
         
    .EXAMPLE
        PS C:\> Get-D365DotNetMethod -Assembly "C:\AOSService\PackagesLocalDirectory\ElectronicReporting\bin\Microsoft.Dynamics365.LocalizationFrameworkForAx.dll" -TypeName "ERTextFormatExcelFileComponent" -Name "*parm*"
         
        Will get all methods that fits the search "*parm*", from the "ERTextFormatExcelFileComponent" class, from the assembly file
         
    .EXAMPLE
        PS C:\> Get-D365DotNetClass -Name "ERTextFormatExcelFileComponent" -Assembly "*LocalizationFrameworkForAx.dll*" | Get-D365DotNetMethod
         
        Will get all methods, from the "ERTextFormatExcelFileComponent" class, from any assembly file that fits the search "*LocalizationFrameworkForAx.dll*"
         
    .NOTES
        The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages.
         
#>

function Get-D365DotNetMethod {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true, Position = 1 )]
        [Alias('File')]
        [string] $Assembly,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [Alias('MethodName')]
        [string] $Name = "*",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )]
        [Alias('ClassName')]
        [string] $TypeName = "*"

    )
    
    begin {
    }
    
    process {
        Invoke-TimeSignal -Start

        try {
            Write-PSFMessage -Level Verbose -Message "Loading the file" -Target $Assembly
            [Reflection.Assembly]$ass = [Reflection.Assembly]::LoadFile($Assembly)

            $types = $ass.GetTypes()

            foreach ($obj in $types) {
                Write-PSFMessage -Level Verbose -Message "Type name loaded" -Target $obj.Name

                if ($obj.Name -NotLike $TypeName) {continue}

                $members = $obj.GetMethods()

                foreach ($objI in $members) {
                    if ($objI.Name -NotLike $Name) { continue }
                    [PSCustomObject]@{
                        TypeName     = $obj.Name
                        TypeIsPublic = $obj.IsPublic
                        MethodName   = $objI.Name
                    }

                }
            }
        }
        catch {
            Write-PSFMessage -Level Warning -Message "Something went wrong while working on: $Assembly" -ErrorRecord $_
        }
        
        Invoke-TimeSignal -End
    }

    end {
    }

}


<#
    .SYNOPSIS
        Cmdlet to get the current status for the different services in a Dynamics 365 Finance & Operations environment
         
    .DESCRIPTION
        List status for all relevant services that is running in a D365FO environment
         
    .PARAMETER ComputerName
        An array of computers that you want to query for the services status on.
         
    .PARAMETER All
        Set when you want to query all relevant services
         
        Includes:
        Aos
        Batch
        Financial Reporter
        DMF
         
    .PARAMETER Aos
        Switch to instruct the cmdlet to query the AOS (IIS) service
         
    .PARAMETER Batch
        Switch to instruct the cmdlet query the batch service
         
    .PARAMETER FinancialReporter
        Switch to instruct the cmdlet query the financial reporter (Management Reporter 2012)
         
    .PARAMETER DMF
        Switch to instruct the cmdlet query the DMF service
         
    .EXAMPLE
        PS C:\> Get-D365Environment -All
         
        Will query all D365FO service on the machine
         
    .EXAMPLE
        PS C:\> Get-D365Environment -ComputerName "TEST-SB-AOS1","TEST-SB-AOS2","TEST-SB-BI1" -All
         
        Will query all D365FO service on the different machines
         
    .EXAMPLE
        PS C:\> Get-D365Environment -Aos -Batch
         
        Will query the Aos & Batch services on the machine
         
    .NOTES
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-D365Environment {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 1 )]
        [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] $Batch,

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

        [Parameter(Mandatory = $false, ParameterSetName = 'Specific', Position = 5 )]
        [switch] $DMF
    )

    if ($PSCmdlet.ParameterSetName -eq "Specific") {
        $All = ![switch]::Present
    }

    if (!$All.IsPresent -and !$Aos.IsPresent -and !$Batch.IsPresent -and !$FinancialReporter.IsPresent -and !$DMF.IsPresent) {
        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) {
        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 environment configs
         
    .DESCRIPTION
        Get all environment configuration objects from the configuration store
         
    .PARAMETER Name
        The name of the environment you are looking for
         
        Default value is "*" to display all environment configs
         
    .EXAMPLE
        PS C:\> Get-D365EnvironmentConfig
         
        This will show all environment configs
         
    .NOTES
         
        You will have to run the Initialize-D365Config cmdlet first, before this will be capable of working.
         
#>

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

    )
    
    $Environments = [hashtable](Get-PSFConfigValue -FullName "d365fo.tools.environments")
        
    foreach ($item in $Environments.Keys) {
        if ($item -NotLike $Name) { continue }
        $temp = [ordered]@{Name = $item}
        $temp += $Environments[$item]
        [PSCustomObject]$temp
    }
}


<#
    .SYNOPSIS
        Get the D365FO environment settings
         
    .DESCRIPTION
        Gets all settings the Dynamics 365 for Finance & Operations environment uses.
         
    .EXAMPLE
        PS C:\> Get-D365EnvironmentSetting
         
        This will get all details available for the environment
         
    .EXAMPLE
        PS C:\> Get-D365EnvironmentSetting | Format-Custom -Property *
         
        This will get all details available for the environment and format it to show all details in a long custom object.
         
    .NOTES
        The cmdlet wraps the call against a dll file that is shipped with Dynamics 365 for Finance & Operations.
        The call to the dll file gets all relevant details for the installation.
         
        Author: Rasmus Andersen (@ITRasmus)
         
#>

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

    Get-ApplicationEnvironment
}


<#
    .SYNOPSIS
        Returns Exposed services
         
    .DESCRIPTION
        Function for getting which sevices there are exposed from D365
         
    .PARAMETER ClientId
        Client Id from the AppRegistration
         
    .PARAMETER ClientSecret
        Client Secret from the AppRegistration
         
    .PARAMETER D365FO
        Url fro the D365 including Https://
         
    .PARAMETER Authority
        The Authority to issue the token
         
    .EXAMPLE
        PS C:\> Get-D365ExposedService -ClientId "MyClientId" -ClientSecret "MyClientSecret"
         
        This will show a list of all the services that the D365FO instance is exposing.
         
    .NOTES
        Idea taken from http://www.ksaelen.be/wordpresses/dynamicsaxblog/2016/01/dynamics-ax-7-tip-what-services-are-exposed/
         
        Author: Rasmus Andersen (@ITRasmus)
         
#>

function Get-D365ExposedService
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true, Position = 1 )]
        [string] $ClientId,
        [Parameter(Mandatory = $true, Position = 2 )]
        [string] $ClientSecret,
        [Parameter(Mandatory = $false, Position = 3 )]
        [string] $D365FO,
        [Parameter(Mandatory = $false, Position = 4 )]
        [string] $Authority
    )

    if($D365FO -eq "") {
        $D365FO = $(Get-D365Url).Url
    }
    if($Authority -eq "") {
        $Authority = Get-InstanceIdentityProvider
    }

    Write-PSFMessage -Level Verbose -Message "Importing type 'Microsoft.IdentityModel.Clients.ActiveDirectory.dll'"
    $null = add-type -path "$script:ModuleRoot\internal\dll\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" -ErrorAction Stop
    

    $url = $D365FO + "/api/services"

    Write-PSFMessage -Level Verbose -Message "D365FO : $D365FO"
    Write-PSFMessage -Level Verbose -Message "Url : $url"
    Write-PSFMessage -Level Verbose -MEssage "Authority : $Authority"
    
    $authHeader = New-AuthorizationHeader $Authority $ClientId  $ClientSecret $D365FO

    [System.Net.WebRequest] $webRequest  = New-WebRequest $url $authHeader "GET"

    $response = $webRequest.GetResponse()

    if ($response.StatusCode -eq [System.Net.HttpStatusCode]::Ok) {

        $stream = $response.GetResponseStream()
    
        $streamReader = New-Object System.IO.StreamReader($stream);
        
        $exposedServices = $streamReader.ReadToEnd()
        $streamReader.Close();
    
    }
    else {
        $statusDescription = $response.StatusDescription
        throw "Https status code : $statusDescription"
    }

    $exposedServices
}


<#
    .SYNOPSIS
        Get installed hotfix
         
    .DESCRIPTION
        Get all relevant details for installed hotfix
         
    .PARAMETER BinDir
        The path to the bin directory for the environment
         
        Default path is the same as the AOS Service PackagesLocalDirectory\bin
         
    .PARAMETER PackageDirectory
        Path to the PackagesLocalDirectory
         
        Default path is the same as the AOS Service PackagesLocalDirectory
         
    .PARAMETER Model
        Name of the model that you want to work against
         
        Accepts wildcards for searching. E.g. -Model "*Retail*"
         
        Default value is "*" which will search for all models
         
    .PARAMETER Name
        Name of the hotfix that you are looking for
         
        Accepts wildcards for searching. E.g. -Name "7045*"
         
        Default value is "*" which will search for all hotfixes
         
    .PARAMETER KB
        KB number of the hotfix that you are looking for
         
        Accepts wildcards for searching. E.g. -KB "4045*"
         
        Default value is "*" which will search for all KB's
         
    .EXAMPLE
        PS C:\> Get-D365InstalledHotfix
         
        This will display all installed hotfixes found on this machine
         
    .EXAMPLE
        PS C:\> Get-D365InstalledHotfix -Model "*retail*"
         
        This will display all installed hotfixes found for all models that matches the search for "*retail*" found on this machine
         
    .EXAMPLE
        PS C:\> Get-D365InstalledHotfix -Model "*retail*" -KB "*43*"
         
        This will display all installed hotfixes found for all models that matches the search for "*retail*" and only with KB's that matches the search for "*43*" found on this machine
         
    .NOTES
        This cmdlet is inspired by the work of "Ievgen Miroshnikov" (twitter: @IevgenMir)
         
        All credits goes to him for showing how to extract these informations
         
        His blog can be found here:
        https://ievgensaxblog.wordpress.com
         
        The specific blog post that we based this cmdlet on can be found here:
        https://ievgensaxblog.wordpress.com/2017/11/17/d365foe-get-list-of-installed-metadata-hotfixes-using-metadata-api/
         
        Author: M�tz Jensen (@Splaxi)
         
#>

function Get-D365InstalledHotfix {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 1 )]
        [string] $BinDir = "$Script:BinDir\bin",

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 2 )]
        [string] $PackageDirectory = $Script:PackageDirectory,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 3 )]
        [string] $Model = "*",

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

        [Parameter(Mandatory = $false, ParameterSetName = 'Default', Position = 5 )]
        [string] $KB = "*"

    )

    begin {
    }

    process {
        $files = @(Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.AX.Metadata.Storage.dll",
            Join-Path -Path $BinDir -ChildPath "Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll")
        
        if(-not (Test-PathExists -Path $files -Type Leaf)) {
            return
        }

        Add-Type -Path $files

        Write-PSFMessage -Level Verbose -Message "Testing if the cmdlet is running on a OneBox or not." -Target $Script:IsOnebox
        if ($Script:IsOnebox) {
            Write-PSFMessage -Level Verbose -Message "Machine is onebox. Will continue with DiskProvider."

            $diskProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.DiskProvider.DiskProviderConfiguration
            $diskProviderConfiguration.AddMetadataPath($PackageDirectory)
            $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory
            $metadataProvider = $metadataProviderFactory.CreateDiskProvider($diskProviderConfiguration)

            Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider
        }
        else {
            Write-PSFMessage -Level Verbose -Message "Machine is NOT onebox. Will continue with RuntimeProvider."

            $runtimeProviderConfiguration = New-Object Microsoft.Dynamics.AX.Metadata.Storage.Runtime.RuntimeProviderConfiguration -ArgumentList $Script:PackageDirectory
            $metadataProviderFactory = New-Object Microsoft.Dynamics.AX.Metadata.Storage.MetadataProviderFactory
            $metadataProvider = $metadataProviderFactory.CreateRuntimeProvider($runtimeProviderConfiguration)

            Write-PSFMessage -Level Verbose -Message "MetadataProvider initialized." -Target $metadataProvider
        }