Functions/AppManagement/Install-BcApp.ps1

<#
    .Synopsis
    Installs Microsoft Dynamics 365 Business Central AL Extension(s) on a Busines Central environment.
    .Description
    This script can install AL extension(s). It's compatible with single- and multi-tenant installations.
 
    The script is executed in two phases: Installation initialization and the installation itself.
    Initialization:
        - Creates a log file.
        - Imports required PowerShell modules.
        - Starts the server instance if not started yet.
        - Sorts the to-install apps in the right installation order.
        - Reads required parameters from the BC server instance.
        - Gathers and displays general environment details.
        - Displays apps to-install and current published apps.
        - Updates Business Central software license.
        - Validates environment preconditions.
        - Asks user confirmation to start installation.
    Installation:
        - Starts installation for each app in the right installation order.
        - Deploys the app in every tenant unless other specified.
            o Publish-NavApp
            o Determine the next installation procedure
            o Uninstall-NavApp (previous version)
            o Sync-NavApp
            o Start-NavAppDataUpgrade or Install-NavApp
            o Set application version in database (if the app is a base app).
            o Unpublish-NavApp (previous app)
        - Restarts BC server tier.
    .Example
    Install-BcApp -ServerInstance 'BC150Test' -SoftwarePath (Join-Path $PSScriptRoot 'Extensions') -ModulePath (Join-Path $PSScriptRoot 'PowerShell')
    .Example
    Install-BcApp -ServerInstance 'BC150Test' -SoftwarePath 'D:\Folder\Software' -ModulePath 'D:\PowerShell\Modules'
    .Example
    Install-BcApp `
        -ServerInstance 'BC150Test' `
        -Tenant 'tenant1' `
        -SoftwarePath (Join-Path $PSScriptRoot 'Extensions') `
        -ModulePath (Join-Path $PSScriptRoot 'PowerShell') `
        -BaseAppId @('d809cdc6-44fd-485c-b9ed-2841276bbf32', 'a5fcb2f3-fce3-47a5-976a-e837599ae46d') `
        -ErrorAction Continue `
        -SkipVerification
    # This will deploy the app files from the folder Extensions relative to the script root path.
    # The tenant is specified. This indicates the environment is Multi-Tenant AND the app(s) should ONLY be deployed in tenant1.
    # The BaseAppId is changed to the BaseAppId of 4PS Construct W1 and 4PS Construct NL, the 4PS base apps.
#>

function Install-BcApp {
    [CmdletBinding()]
    Param(
        # Specifies the name of a Business Central Server Instance. E.g. 'BC150' or 'NST150Test'
        [Parameter(Mandatory=$true)]
        [string] $ServerInstance,  

        # Path to the folder that contains the app file(s) to install.
        [Parameter(Mandatory=$true)]
        [string] $SoftwarePath,

        # Path to the PowerShell deployment module folder. e.g. c:\powershell\modules.
        # In this folder should the modules be pressent:
        # 'FpsDeployment\FpsBcDeployment.psd1'
        # 'FpsGeneral\FpsGeneral.psd1'
        [string] $ModulePath,

        # The customers Business Central software license file. E.g. 'C:\License\CustomerLicense.flf'
        # Important note for multi-tenant environments: When there are multiple tenants mounted on the serverinstance
        # and with the $Tenants parameter there are zero, two or more tenants specified; the license will be installed in
        # multiple tenants. If every tenant has it's own license, don't use this functionality. Upload it seperately.
        [string] $LicensePath,

        # Tenant parameter can only be used with a Business Central multi tenant installation.
        # If no tenant is specified and the environment is multi-tenant the app will be published in all tenants.
        # E.g. 'tenant1' or @('tenant1', 'tenant2') to specify multiple tenants.
        [string[]] $Tenants,      

        # Use $AppScope to set the default scope for new installed apps.
        # If there is a previous version of an app installed the scope of this app will be used.
        # Global: The extension is published for all tenants on the server.
        # Tenant: The extension is published into the per-tenant scope.
        [string] $AppScope,        

        # For Microsoft Partners with their own base app, supply the BaseAppId. Default value is the Microsoft Base App.
        [string[]] $BaseAppId = @('437dbf0e-84ff-417a-965d-ed2bb9650972'),

        # Location to write the logfile to. E.g. 'C:\BcInstallation\Log'
        # Default location is '?:\ProgramData\4ps\bcdeployment'.
        [string] $LogFilePath = (Join-Path -Path $env:ProgramData -ChildPath '4ps\bcdeployment'),

        # When an extension is updated all dependend extensions will be uninstalled during the upgrade.
        # Default the dependend apps will be reïnstalled after the installation completed.
        # With $SkipAutoInstallChildApps on $true the dependend apps will not be reïnstalled.
        [switch] $SkipAutoInstallChildApps, 

        # For every 1 MB appfile (runtime) size roughtly 35 MB free memory is required for the serverinstance to compile the app.
        # For every 1 MB appfile (full) seze roughly 180 MB free memory is required.
        # This scripts validates if there is enough free memory to compile the largest to-install app.
        # This validation can be disabled by setting $SkipMemoryCheck to $true.
        [switch] $SkipMemoryCheck,

        # Default after the script initialisation the user is asked to continue or not.
        # This question can be disabled by setting $SkipConfirmation to $true.
        [switch] $SkipConfirmation,

        # Forces the deployment to run without verifying the authenticode signature.
        # Not recommendend in production environments.
        [switch] $SkipVerification,

        # Default the Business Central Service is restarted after installation.
        # To disable the service restart after the installation set $SkipServiceRestart to $true.
        [switch] $SkipServiceRestart,

        # Default the previous app will be unpublish after a succesfull installation.
        # To keep the previous app published in the database set $KeepPreviousApp to $true.
        [switch] $KeepPreviousApp,

        # Forces the installation of the license file in all tenants.
        [switch] $ForceLicense
    )
    "
     ____ _____ _____ _ _
    | _ \ / ____| | __ \ | | | |
    | |_) | | | | | | ___ _ __ | | ___ _ _ _ __ ___ ___ _ __ | |_
    | _ <| | | | | |/ _ \ '_ \| |/ _ \| | | | '_ `` _ \ / _ \ '_ \| __|
    | |_) | |____ | |__| | __/ |_) | | (_) | |_| | | | | | | __/ | | | |_
    |____/ \_____| |_____/ \___| .__/|_|\___/ \__, |_| |_| |_|\___|_| |_|\__|
                                | | __/ |
       4PS v{0} |_| |___/
    "
 -f $MyInvocation.MyCommand.Module.Version | Write-Host

    # Validate PowerShell version
    if (-not $PSVersionTable.PSVersion.Major -ge '5') {

        $Message = 'Powershell 5.0 or higher required to continue. Current version is {0}.{1}' -f `
            $PSVersionTable.PSVersion.Major,
            $PSVersionTable.PSVersion.Minor
        throw $Message
    }
    'PowerShell version {0} is compatible.' -f $PSVersionTable.PSVersion.ToString() | Write-Host

    # Validate if PowerShell is started as administrator
    $WindowsIdentity = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent())
    $Elevated = ($WindowsIdentity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
    if (-not $Elevated) {
        $Message  = 'You need administrative privileges to deploy Business Central extensions. '
        $Message += 'Start the script in a Powershell session launched as administrator.'
        throw $Message
    }
    'Script is executed as administrator.' | Write-Host

    # Validate if script is executed in a 64 bit process.
    if(-not [Environment]::Is64BitProcess){
        $Message = 'This script needs to be executed as a 64 bit process. Current process is x86 (32bit).'
        throw $Message
    }
    'Session is 64 bit.' | Write-Host
    
    try{ Stop-Transcript }catch{}

    # Create logfile
    $LogFile = Join-Path -Path $LogFilePath -ChildPath ('{0}_{1}.log' -f 
                                (Get-Date).ToString('yyyy-MM-dd_HH.mm.ss'), 
                                $ServerInstance)
    
    New-Item -Path $LogFile -ItemType File -Force | Out-Null

    # Remove old logfiles (keep max 10 logfiles per ServerInstance)
    $Regex = '.*_{0}\.log' -f $ServerInstance
    $LogFiles = Get-ChildItem $LogFilePath -File -Filter '*.log' | Where-Object -Property Name -Match $Regex
    if($LogFiles.Count -gt 10){
        $Exclude = $LogFiles | Sort-Object -Property CreationTime -Descending | Select-Object -First 10
        Get-ChildItem (Split-Path $LogFile -Parent) -Exclude $Exclude | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    }

    Start-Transcript -path $LogFile -append


    ### Import required modules
    'Importing required PowerShell modules...' | Write-host
    if($ModulePath){
        $GeneralModulePath = (Join-Path  $ModulePath 'FpsGeneral\FpsGeneral.psd1')
        Import-Module $GeneralModulePath -DisableNameChecking -Global -Force
    } elseif(-not (Get-Module FpsBcDeployment) -or -not (Get-Module FpsGeneral)){
        $Message = 'Please import PowerShell module FpsBcDeployment and FpsGeneral before executing this cmdlet, or supply the path to the module.'
        throw $Message
    }

    # Validate if the NST exists
    Check-ServiceNotExistsError -ServerInstance $ServerInstance -ErrorAction Stop

    # Import Business Central Modules
    Import-BcModule `
        -ServerInstance $ServerInstance `
        -ManagementModule `
        -AppManagementModule `
        -Force
        
    $TotalTime = Write-StartProcessLine -StartLogText 'Microsoft Dynamics 365 Business Central AL Extension(s) deployment on a Busines Central environment.'

    $TaskStartTime = Write-StartProcessLine -StartLogText 'Validating Business Central server instance state.'
        # Validate if NST is running
        if((Get-NAVServerInstance -ServerInstance $ServerInstance).State -ne 'Running'){
            'ServerInstance {0} is not running. Starting service...' -f $ServerInstance | Write-Host
            Set-NAVServerInstance -ServerInstance $ServerInstance -Start -ErrorAction Stop
            
        } else {
            'ServerInstance {0} is running.' -f $ServerInstance | Write-Host
        }

        Wait-BcServerInstanceMountingTenants `
            -ServerInstance $ServerInstance 
    Write-EndProcessLine -TaskStartTime $TaskStartTime


    ### Get installation details
    $TaskStartTime = Write-StartProcessLine -StartLogText 'Retreiving installation details.'
        # Get system info
        $OperatingSystem = Get-WmiObject -Class Win32_OperatingSystem
        $TotalMemory = [math]::round($OperatingSystem.TotalVisibleMemorySize/1024/1024)
        $FreeMemory  = [math]::round(($OperatingSystem.FreePhysicalMemory/1024/1024), 1)
        $Processor   = Get-WmiObject Win32_Processor
        
        $OsInfo  = @("`n")
        $OsInfo += 'Operating System : {0} {1}' -f $OperatingSystem.Caption, $OperatingSystem.Version
        $OsInfo += 'PowerShell Version : {0}' -f $PSVersionTable.PSVersion.ToString()
        $OsInfo += 'Total Memory : {0} GB' -f $TotalMemory
        $OsInfo += 'Free Memory : {0} GB' -f $FreeMemory
        $OsInfo += 'CPU Name : {0}' -f $Processor.Name
        $OsInfo += 'CPU Cores : {0}' -f $Processor.NumberOfCores
        $OsInfo += 'CPU Threads : {0}' -f $Processor.NumberOfLogicalProcessors
        $OsInfo += 'CPU MaxClockSpeed : {0}' -f $Processor.MaxClockSpeed
        $OsInfo += "`n"
        
        # Get appfiles in right installation order
        $Apps = Get-AppDependencyOrder -Path $SoftwarePath -ErrorAction Stop

        # Get tenants
        if(-not $Tenants){
            $Tenants = (Get-NAVTenant -ServerInstance $ServerInstance).Id
        }

        # Write environment installation to host
        $Title = 'Installation Details' 
        $Body =  "`n"
        $Body += '*** Operating System ***{0}' -f ($OsInfo | Out-String)
        $Body += '*** Business Central environment ***{0}' -f 
                    (Get-BCServerInstance -ServerInstance $ServerInstance | Out-String)
                
        $Body += "*** Targeted Tenants ***{0}" -f 
                    (Get-NAVTenant -ServerInstance $ServerInstance | `
                    Select-Object -Property Id, State, AllowAppDatabaseWrite, DatabaseName, DatabaseServer, TenantDataVersion | `
                    Where-Object -Property Id -in $Tenants | Out-String)

        $Body += "*** Apps to deploy ***`n{0}`n" -f 
                    ($Apps | Select-Object -Property Name, Publisher, Version | Out-String)
        
        $Body += "*** Current published apps ***`n"
        
        $Tenants | ForEach-Object {
            $Body += "Apps published in tenant {0}:`n{1}`n" -f $_, 
                        (Get-NavAppInfo `
                            -ServerInstance $ServerInstance `
                            -Tenant $_ `
                            -TenantSpecificProperties | Sort-Object -Property Name, Version | `
                                Select-Object -Property Name, Publisher, IsPublished, SyncState, NeedsUpgrade, IsInstalled, ExtensionDataVersion, Version, AppId | `
                                Format-Table | Out-String)
        }
        
        $Body += "*** Bound Parameters ***`n{0}" -f ($PSCmdlet.MyInvocation.BoundParameters | Out-String)
        
        Write-host (Write-DebugInfoToOutput -MessageTitle $Title -MessageBody $Body)
    Write-EndProcessLine -TaskStartTime $TaskStartTime


    ### Validations on preconditions before installation
    $TaskStartTime = Write-StartProcessLine -StartLogText "Validating environment." 
        # Validate if enough free memory is available.
        # Publishing large extensions in Business Central consumes a lot of memory, because the whole application is compiled on publishing.
        if(-not $SkipMemoryCheck){
            # Get the filesize of the largest app file
            $FileSize = ($Apps | Sort-Object -Property Length -Descending | Select-Object -First 1).Length
            
            # Calculate minimal free memory in GB. Every 1 MB appfile equals roughly 180 MB free memory, 1 MB equals ~35 MB for runtime apps.
            $MinimalFreeMemory = [math]::round($FileSize /1MB, 2) * 35
            $MinimalFreeMemory = [math]::round($MinimalFreeMemory * 1MB /1GB, 1)

            if([decimal] $FreeMemory -lt [decimal] $MinimalFreeMemory){
                $Message  = 'Not enough free memory available. System has {0} GB of the {1} GB memory available. ' -f 
                                $FreeMemory, $TotalMemory
                $Message += 'Atleast {0} GB free memory is required' -f $MinimalFreeMemory
                throw $Message
            }

            '{0} GB free memory is above the minimal threshold of {1} GB.' -f
                $FreeMemory, $MinimalFreeMemory | Write-Host
        }

        # Validate if tenants are valid
        $MountedTenants = Get-NAVTenant -ServerInstance $ServerInstance
        $Tenants | ForEach-Object {

            if($_ -notin $MountedTenants.Id){
                $Message = "Supplied tenant '{0}' is not mounted on the ServerInstance '{1}'." -f 
                                $_, $ServerInstance
                throw $Message
            } 
            
            $TenantState = ($MountedTenants | Where-Object -property Id -eq $_).State
            if($TenantState -ne 'Operational'){
                $Message  = "The tenant '{0}' doesn't have the status 'Operational'. The tenant status is {1}.`n" -f 
                                $_, $TenantState
                $Message += "Please make the tenant operational before starting the deployment."
                throw $Message
            }
        }
        'Supplied tenant(s) are mountend and operational on ServerInstance {0}' -f $ServerInstance | Write-Host
    Write-EndProcessLine -TaskStartTime $TaskStartTime


    ### Update Business Central Software License
    $TaskStartTime = Write-StartProcessLine -StartLogText "Updating Business Central Software License." 
        $ImportLicense = $false

        # Validate if LicensePath is set, the filepath is valid and the file extension is .flf. If all true read the file.
        if($LicensePath){
            if((Test-Path $LicensePath)){
                if((Get-Item -Path $LicensePath).Extension -eq '.flf' ){
                    $NewLicense = Get-LicenseDetails -RawLicense (Get-Content $LicensePath -Raw)
                    $Message  = @('')
                    $Message += "*** Supplied license file details ***`n"
                    $Message += 'License Path : {0}' -f $LicensePath
                    $Message += 'Account Number : {0}' -f $NewLicense.AccountNumber
                    $Message += 'Licensed To : {0}' -f $NewLicense.LicensedTo
                    $Message += 'Created Date : {0}' -f $NewLicense.CreatedDate.ToShortDateString()
                    if(-not [string]::IsNullOrEmpty($NewLicense.ExpireDate)){
                        $Message += 'Expire Date : {0}' -f $NewLicense.ExpireDate.ToShortDateString()
                    }
                    $Message += ''
                    $Message | Write-Host

                    $ElapsedTime = ((Get-Date) - $NewLicense.CreatedDate)
                    if($ElapsedTime.Days -gt 60){
                        $Message = 'The supplied license is created {0} days ago. It is recommended to use a more recent license.' -f 
                                      $ElapsedTime.Days
                        Write-Warning $Message
                    }
                } else {
                    throw 'Supplied license is not a Business Central license. Expected a path to a .flf license file.'
                }
            } else {
                throw ('Could not find the Business Central license file on path: {0}.' -f $LicensePath)
            }
        } else {
            'Import new license skipped. No path to the Business Central Software License file is set.' | Write-Host
        }
        
        if($Tenants.Count -ge 2 -and (Test-Path Variable:\NewLicense) -eq $true){
            $Message = @()
            $Message += 'Installation is initiated for multiple tenants and ONE license file is specified.'
            $Message += 'The specified license file will be imported in EVERY tenant database.'
            $Message += 'If the tenants have separate licenses: Upload them seperatly and do not use the upload license functionalitiy of this cmdlet OR'
            $Message += 'start the deployment for every tenant seperatly with each their own license file. '
            $Message += 'To install the same license in all tenants set the -ForceLicense switch.'
            if ($ForceLicense){
                Write-Warning ($Message | Out-String)
            } else {
                throw ($Message | Out-String)
            }
        }

        foreach($Tenant in $Tenants){
            # Read the active license from the ServerInstance.
            $CurrentLicense = Get-LicenseDetails -RawLicense (Export-NAVServerLicenseInformation `
                -ServerInstance $ServerInstance `
                -Tenant $Tenant) 

            $Message  = @('')
            $Message += "*** Database license file details ***`n"
            $Message += 'Tenant : {0}' -f $Tenant
            $Message += 'Account Number : {0}' -f $CurrentLicense.AccountNumber
            $Message += 'Licensed To : {0}' -f $CurrentLicense.LicensedTo
            $Message += 'Created Date : {0}' -f $CurrentLicense.CreatedDate.ToShortDateString()
            if(-not [string]::IsNullOrEmpty($NewLicense.ExpireDate)){
                $Message += 'Expire Date : {0}' -f $CurrentLicense.ExpireDate.ToShortDateString()
            }
            $Message += ''
            $Message | Write-Host
            
            if(-not $NewLicense){
                $ElapsedTime = ((Get-Date) - $CurrentLicense.CreatedDate)
                if($ElapsedTime.Days -gt 60){
                    $Message = 'The license in the database is created {0} days ago. It is recommended to use a more recent license.' -f 
                                    $ElapsedTime.Days
                    Write-Warning $Message
                }
            }

            if($NewLicense){
                if($NewLicense.AccountNumber -ne $CurrentLicense.AccountNumber){
                    $Message = @()
                    $Message += 'Supplied license and license in the database are licensed to a different VOICE accounts.'
                    $Message += 'Supplied license account number: {0}, Licensed To: {1}.' -f 
                                    $NewLicense.AccountNumber, 
                                    $NewLicense.LicensedTo
                    $Message += 'License in database account number: {0}, Licensed To: {1}.' -f 
                                    $CurrentLicense.AccountNumber, 
                                    $CurrentLicense.LicensedTo
                    if ($ForceLicense){
                        Write-Warning ($Message | Out-String)
                    } else {
                        throw ($Message | Out-String)
                    }
                }
                
                if($NewLicense.CreatedDate -lt $CurrentLicense.CreatedDate)
                {
                    $Message = 'Suplied license is older than the active license in the database.'
                    if ($ForceLicense){
                        Write-Warning $Message
                    } else {
                        throw $Message
                    }
                }
    
                if($NewLicense.CreatedDate -eq $CurrentLicense.CreatedDate)
                {
                    'License is the same as the active license in the database.' | Write-Host
                    if (-not $ForceLicense){ 
                        'Import new license skipped.' | Write-Host
                        continue 
                    }
                }
    
                if($NewLicense.CreatedDate -gt $CurrentLicense.CreatedDate)
                {
                    'Supplied license file is more recent than the active license in the database' | Write-Host
                }
                    
                'Importing license into the database...' | Write-Host
                
                $ImportLicense = $true

                if($Tenant -eq 'default'){
                    $LicenseDatabase = 'NavDatabase'
                } else {
                    $LicenseDatabase = 'Tenant'
                }
                Import-NAVServerLicense `
                    -Tenant $Tenant `
                    -ServerInstance $ServerInstance `
                    -LicenseFile $LicensePath `
                    -Database $LicenseDatabase `
                    -WarningAction SilentlyContinue
            } # End if $NewLicense
            
        } # End foreach tenant
        

        if($ImportLicense){
            'Restarting ServerInstance {0} to activate license...' -f $ServerInstance | Write-Host
            Set-NavServerInstance `
                -ServerInstance $ServerInstance `
                -Restart

            Wait-BcServerInstanceMountingTenants `
                -ServerInstance $ServerInstance
        }
    Write-EndProcessLine -TaskStartTime $TaskStartTime


    $Message  = 'This script does NOT make a SQL backup from the Business Central database. ' 
    $Message += 'Please make sure you have a valid backup before continuing.' 
    Write-Warning -Message $Message

    if(-not $SkipConfirmation){
        'All validations have passed. Do you want to continue the installation?' | Write-Host -ForegroundColor Green
        Show-MessageBox `
            -MessageType InShell `
            -Button OKCancel `
            -Message $Title `
            -ErrorMsg 'User canceled installation.' `
            -Throw
    }


    "
         _____ _ _ _ _ _
        |_ _| | | | | | | | (_)
          | | _ __ ___| |_ __ _| | | __ _| |_ _ ___ _ __
          | | | '_ \/ __| __/ _`` | | |/ _`` | __| |/ _ \| '_ \
         _| |_| | | \__ \ || (_| | | | (_| | |_| | (_) | | | |
        |_____|_| |_|___/\__\__,_|_|_|\__,_|\__|_|\___/|_| |_|
                            Starts here
    "
 | Write-Host

    ### Execute installation script
    $TaskStartTime = Write-StartProcessLine -StartLogText "Installing Business Central Extensions." 

    # Loop through all the app files and install them. Appfiles are already sorted on right installation order.

    foreach ($AppFilePath in $Apps.Path) {
        # Set parameters for new app version
        $NewApp     = Get-NAVAppInfo -Path $AppFilePath
        $AppGuid    = $NewApp.AppId.Value.Guid
        $AppVersion = $NewApp.Version.Tostring()
        $AppName    = $NewApp.Name

        Write-host (Write-DebugInfoToOutput -MessageTitle ('Installation: {0} v{1}' -f $AppName, $AppVersion)) -ForegroundColor Green

        # Installation for a base app has additional installation steps compared to a regular extension.
        if($AppGuid -in $BaseAppId){        
            'Extension {0} is marked as BaseApp.' -f $AppName | Write-Host
            $AppIsBaseApp = $true
        } else {
            'Extension is not marked as BaseApp' | Write-Host
            $AppIsBaseApp    = $false
        }

        foreach($Tenant in $Tenants){
            $SkipPublish     = $false
            $SkipUnInstall   = $false
            $SkipSync        = $false
            $SkipDataUpgrade = $false
            $SkipInstall     = $false
            $SkipUnpublish   = $false

            ### START *** App file installation initialization ***
            $AppDeployStartTime = Write-StartProcessLine -StartLogText ("*** Installation started for app {0} on tenant {1} ***" -f $AppName, $Tenant)
            
            $SubTaskStartTime = Write-StartProcessLine -StartLogText ('Scan for earlier published apps for app {0} in tenant {1}' -f $AppName, $Tenant)
                'Location app file: {0}' -f $AppFilePath | Write-Host
            
                # Set parameters for the current published app if there is already a version of the app installed.
                $CurrentApp = ''
                $CurrentAppVersion = ''
                $CurrentAppName = ''

                $CurrentApp = Get-NAVAppInfo `
                                    -ServerInstance $ServerInstance `
                                    -Id $AppGuid `
                                    -Tenant $Tenant `
                                    -TenantSpecificProperties

                $CurrentApp | ForEach-Object {
                    # Validate if supplied app file contains a newer app than the current published apps.
                    if ([version] $_.Version -gt [version] $AppVersion){
                        $Message = "Version {0} of the current published app is higher or equal to the version {1} of the supplied app file:" -f
                                        $_.Version.Tostring(),
                                        $AppVersion
                        $Message | Write-Warning
                    }
                }

                # If multiple versions of the same app are published, select one as current app.
                if($CurrentApp.Count -gt 1){
                    'Multiple ({0}) versions of app {1} are already published. Selecting appropriate version as previous version.' -f 
                        $CurrentApp.Count, $AppName | Write-Host
                    
                    # Select the app that is currently installed
                    if (($CurrentApp | Where-Object -Property IsInstalled -eq $true)){
                        $CurrentApp = $CurrentApp | Where-Object -Property IsInstalled -eq $true
                    }
                    # Select the version that correspond with the current tenant data version as current app.
                    elseif($CurrentApp[0].ExtensionDataVersion -in $CurrentApp.Version){
                        $CurrentApp = $CurrentApp | Where-Object -Property Version -eq $CurrentApp[0].ExtensionDataVersion
                    } 
                    # If neither are present, select the oldest version of the app
                    else {
                        $CurrentApp = $CurrentApp | Sort-Object -Property Version | Select-Object -First 1
                    }
                }
                
                if ($CurrentApp) {
                    $CurrentAppVersion = $CurrentApp.Version.Tostring()
                    $CurrentAppName    = $CurrentApp.Name

                    'Previous version of app {0} found:{1}' -f `
                        $AppName, 
                        ($CurrentApp | Select-Object -Property Name, Publisher, Version | Out-String) | Write-Host

                    if([version] $AppVersion -lt [version] $CurrentAppVersion){
                        
                        $Message = 'The supplied app version {0} is lower than the already install app version {1}. Skipping installation.' -f `
                                        $AppVersion, $CurrentAppVersion

                        if($AppIsBaseApp){
                            throw $Message
                        } else {
                            Write-Warning $Message
                            $SkipPublish     = $true
                            $SkipUnInstall   = $true
                            $SkipSync        = $true
                            $SkipDataUpgrade = $true
                            $SkipInstall     = $true
                            $SkipUnpublish   = $true
                        }                        
                    }

                    if ([version] $AppVersion -eq [version] $CurrentAppVersion){
                        $SkipPublish   = $true
                        $SkipUnpublish = $true
                        'SkipPublish and SkipUnpublish are set to true,' | Write-Host
                        ' version {0} of the supplied app is equal to the currently installed app.' -f $AppVersion | Write-Host
                        
                    }

                    
                } else {
                    'SkipUnpublish set to true,' | Write-Host
                    ' no previous published app {0} is found.' -f $AppName | Write-Host
                    $SkipUnpublish = $true
                }

                if($KeepPreviousApp){
                    'SkipUnpublish set to true, because switch KeepPreviousApp is enabled.' | Write-Host
                    $SkipUnpublish = $true
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime

            # Get app scope if not set.
            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Set app scope.' 
                if ($CurrentApp) {
                    $AppScope = $CurrentApp.Scope
                    'The extension scope is set to the same level as the previous app: {0}' -f $CurrentApp.Scope | Write-Host
                } elseif (-not $AppScope) {
                    'The extension scope is set to the default value: Tenant' | Write-Host
                    $AppScope = 'Tenant'
                } else {
                    'Appscope is set as installation parameter: {0}' -f $AppScope | Write-Host
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime

            
            # Get the (child) apps that depend on the app that is being installed, to reinstall them after the installation.
            $SubTaskStartTime = Write-StartProcessLine -StartLogText ('Scanning for installed apps that depend on the to-install app {0}.' -f $AppName) 
                $InstalledDependendApps = @()
                $PublishedApps = Get-NAVAppInfo `
                                    -ServerInstance $ServerInstance `
                                    -Tenant $Tenant `
                                    -TenantSpecificProperties
                
                # Check every published app if the guid of the to-install app is pressent in the dependencies.
                foreach ($PublishedApp in $PublishedApps){
                    $AppDependencies = Get-NAVAppInfo `
                                            -ServerInstance $ServerInstance `
                                            -Id $PublishedApp.AppId.Value.Guid `
                                            -Tenant $Tenant `
                                            -TenantSpecificProperties `
                                            -Version $PublishedApp.Version
                    
                    # Exclude not installed child apps, they don't need to be reinstalled.
                    if ($AppDependencies.IsInstalled -ne $True) { continue }
                    
                    foreach($Dependency in $AppDependencies.Dependencies){
                        if($Dependency.AppId.Guid -eq $AppGuid){
                            $InstalledDependendApps += $PublishedApp
                        }
                    }
                }
                if ($InstalledDependendApps.Count -ge 1){
                    "The following apps depend on the app {0} that is being installed and will be {1} during the installation: `n{2}" -f `
                        $AppName, 
                        $(if($SkipAutoInstallChildApps){'uninstalled'} else {'re-installed'}),
                        ($InstalledDependendApps.Name | Out-String) | Write-Host
                } else {
                    'No installed child-apps found.' | Write-Host
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime
            ### END *** App file installation initialization ***
            
            ### START *** App file installation ***
            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Publishing extension.'
                if(-not $SkipPublish){
                    'Publishing {0} App, Guid: {1}, version {2} on server instance {3}.' -f 
                        $AppName, $AppGuid, $AppVersion, $ServerInstance | Write-Host
                    
                    switch ($AppScope) {
                        'Tenant' {  
                            "Publish-NAVApp -ServerInstance '{0}' -Path '{1}' -Tenant '{2}' -Scope '{3}' -SkipVerification:{4}" -f 
                                $ServerInstance, $AppFilePath, $Tenant, $AppScope, $SkipVerification | Write-Host
                            Publish-NAVApp `
                                -ServerInstance $ServerInstance `
                                -Path $AppFilePath `
                                -Tenant $Tenant `
                                -Scope $AppScope `
                                -SkipVerification:$SkipVerification
                        }
                        'Global' {
                            "Publish-NAVApp -ServerInstance '{0}' -Path '{1}' -Scope '{2}' -SkipVerification:{3}" -f 
                                $ServerInstance, $AppFilePath, $AppScope, $SkipVerification | Write-Host
                            Publish-NAVApp `
                                -ServerInstance $ServerInstance `
                                -Path $AppFilePath `
                                -Scope $AppScope `
                                -SkipVerification:$SkipVerification
                        }
                    }
                    
                } else {
                    'Skipped Publish-NAVApp.' | Write-Host
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime


            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Determine which next installation steps are required.'
                # Update $NewApp with tenant specific installation status.
                $NewApp = Get-NAVAppInfo `
                            -ServerInstance $ServerInstance `
                            -Id $AppGuid `
                            -Version $AppVersion `
                            -Tenant $Tenant `
                            -TenantSpecificProperties

                # Check if Sync-NAVApp should be executed.
                if($NewApp.SyncState -eq 'Synced'){
                    'SkipSync is set to true, table definition is already in sync with SQL tables.' | Write-Host
                    $SkipSync = $true
                } else {
                    'Sync-NAVApp should be executed. Syncstate is {0}' -f $NewApp.SyncState | Write-host
                }
                
                # Check if Start-NAVAppDataUpgrade should be executed.
                if([version] $NewApp.ExtensionDataVersion -eq [version] $AppVersion){
                    'SkipDataUpgrade is set to true, extensions data already upgraded.' | Write-Host
                    $SkipDataUpgrade = $true
                } 
                elseif([string]::IsNullOrEmpty($NewApp.ExtensionDataVersion)){
                    'SkipDataUpgrade is set to true, extensionDataVersion is empty.' | Write-Host
                    ' It is the first time this extension is installed.' | Write-Host
                    $SkipDataUpgrade = $true
                }
                else {
                    'Start-NAVAppDataUpgrade should be executed.' | Write-Host
                    ' Extension dataversion in tenant {0} is {1} and should be upgraded to {2}.' -f 
                            $Tenant,
                            $NewApp.ExtensionDataVersion.ToString(), 
                            $AppVersion | Write-host
                }
                
                # Check if there is a previous app that should be uninstalled.
                if([string]::IsNullOrEmpty($CurrentApp)){
                    'SkipUnInstall is set to true, no previous extension found.' | Write-Host
                    $SkipUnInstall = $true
                } 
                elseif ($CurrentApp.IsInstalled -eq $false) {
                    'SkipUnInstall is set to true, previous extension is not installed.' | Write-Host
                    $SkipUnInstall = $true
                } 
                elseif($CurrentApp.IsInstalled -eq $true -and 
                         $SkipSync -eq $true -and 
                         $SkipDataUpgrade -eq $true){
                    'SkipUnInstall is set to true, there is no table sync or dataupgrade planned for the extension.' | Write-Host
                    $SkipUnInstall = $true
                } 
                else {
                    'Previous app will be uninstalled due to a pending table sync or dataupgrade.' | Write-Host
                }

                # Check if Install-NavApp should be executed.
                # Only use Install-NavApp if the new app has not been installed before or if the app is already in sync and upgraded.
                if($NewApp.IsInstalled -eq $true){
                    'SkipInstall is set to true, extension is already installed.' | Write-Host
                    $SkipInstall = $true
                }
                elseif($SkipDataUpgrade -eq $false){
                    'SkipInstall is set to true, Extension will be installed during the dataupgrade.' | Write-Host
                    $SkipInstall = $true
                } 
                
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime
            

            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Uninstalling previous app.'
                # Uninstall current app if applicable. Uninstall app is only required in some cases of table or data sync.
                if(-not $SkipUnInstall){  
                    'Uninstalling {0} App, version {1}.' -f $CurrentAppName, $CurrentAppVersion | Write-Host
                    "Uninstall-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}' -Force" -f
                        $ServerInstance, $CurrentAppName, $CurrentAppVersion, $Tenant | Write-Host
                    Uninstall-NAVApp `
                        -ServerInstance $ServerInstance `
                        -Name $CurrentAppName `
                        -Version $CurrentAppVersion `
                        -Tenant $Tenant `
                        -Force 
                } else {
                    'Skipped Uninstall-NAVApp.' | Write-Host 
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime


            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Syncing database schema for new app.'
                if(-not $SkipSync){  
                    'Execute sync {0} App on server instance {1}' -f $AppName, $ServerInstance | Write-Host
                    "Sync-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Mode '{3}' -Tenant '{4}'" -f
                        $ServerInstance, $AppName, $AppVersion, 'Add', $Tenant | Write-Host
                    Sync-NAVApp `
                        -ServerInstance $ServerInstance `
                        -Name $AppName `
                        -Version $AppVersion `
                        -Mode Add `
                        -Tenant $Tenant
                } else {
                    'Skipped Sync-NAVApp.' | Write-Host 
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime


            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Execute data upgrade for new app.'
                if(-not $SkipDataUpgrade){  
                    'Execute data upgrade for {0} App' -f $AppName | Write-Host
                    'Note: Install-NAVApp is included in Start-NAVAppDataUpgrade' | Write-Host
                    "Start-NAVAppDataUpgrade -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}'" -f
                        $ServerInstance, $AppName, $AppVersion, $Tenant | Write-Host

                    Start-NAVAppDataUpgrade `
                        -ServerInstance $ServerInstance `
                        -Name $AppName `
                        -Version $AppVersion `
                        -Tenant $Tenant
                } else {
                    'Skipped Start-NAVAppDataUpgrade.' | Write-Host 
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime


            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Installing app if not yet installed.'
                if(-not $SkipInstall){  
                    'Installing {0} App' -f $AppName | Write-Host 
                    "Install-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}' -Tenant '{3}' -Force" -f 
                        $ServerInstance, $AppName, $AppVersion, $Tenant

                    Install-NAVApp `
                        -ServerInstance $ServerInstance `
                        -Name $AppName `
                        -Version $AppVersion `
                        -Tenant $Tenant `
                        -Force
                } else {
                    'Skipped Install-NAVApp.' | Write-Host 
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime

            
            
            if ($AppIsBaseApp){
                $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Updating the application version in the database for base app.'       
                
                $BaseAppStatus = Get-NavAppInfo -ServerInstance $ServerInstance -Tenant $Tenant -TenantSpecificProperties
                $BaseAppStatus = $BaseAppStatus | Where-Object {$_.AppId.Value.Guid -eq $AppGuid -and $_.Version -eq $AppVersion}
                $BaseAppStatus | Select-Object -Property AppId, Name, Version, SyncState, NeedsUpgrade, IsInstalled | Write-host
                
                # Only set higher application version if installation of the baseapp is successful.
                if($BaseAppStatus.IsInstalled -eq $false){
                    $Message = 'Updating the base app {0} version {1} failed. Installation canceled.' -f $AppName, $AppVersion
                    throw $Message
                }

                # Update the ApplicationVersion in the application database (the application version displayed in the webclient)
                if(([version] $AppVersion).Major -ge '17'){
                    [string] $nAppVersion = '{0}.{1}.0.0' -f ([version] $AppVersion).Major, ([version] $AppVersion).Minor
                } else {
                    [string] $nAppVersion = $AppVersion
                }
                $ApplicationVersion = (Get-NAVApplication -ServerInstance $ServerInstance).ApplicationVersion
                if ([version] $nAppVersion -gt [version] $ApplicationVersion){
                    
                    'Update {0} app version number from {1} to {2} in the application database.' -f 
                        $AppName, $ApplicationVersion, $nAppVersion | Write-Host   
                    Set-NAVApplication `
                        -ServerInstance $ServerInstance `
                        -ApplicationVersion $nAppVersion `
                        -Force
                    
                    # The dataupgrade needs to be executed for each tenant.
                    # If not the other tenant state becomes 'OperationalDataUpgradePending'
                    (Get-NAVTenant -ServerInstance $ServerInstance).Id | ForEach-Object {
                        'Run the data upgrade for tenant {0} on ServerInstance {1}.' -f $_, $ServerInstance | Write-Host  
                        ' Sync-NAVTenant' | Write-Host  
                        Sync-NAVTenant `
                            -ServerInstance $ServerInstance `
                            -Mode Sync `
                            -Tenant $_ `
                            -Force

                        ' Start-NAVDataUpgrade' | Write-Host  
                        $dataUpgradeParams = @{
                            ServerInstance = $ServerInstance
                            Tenant = $_
                            FunctionExecutionMode = 'Serial'
                            SkipUserSessionCheck = $true
                            SkipAppVersionCheck = $true
                            Force = $true
                        }
                        if (([version] $AppVersion).Major -lt '17'){
                            $dataUpgradeParams["SkipCompanyInitialization"] = $true
                        }
                        Start-NAVDataUpgrade @dataUpgradeParams

                        ' Get-NAVDataUpgrade' | Write-Host  
                        $DataUpgradeState = 'InProgress'
                        while ($DataUpgradeState -eq 'InProgress'){
                            ' Dataupgrade status: {0}' -f $DataUpgradeState | Write-Host
                            $DataUpgradeState = (Get-NAVDataUpgrade -ServerInstance $ServerInstance -Tenant $Tenant).state
                            Start-Sleep -Seconds 3
                        }
                        ' Dataupgrade status: {0}' -f $DataUpgradeState | Write-Host
                    }
                } else {
                    'ApplicationVersion is already {0}.' -f $ApplicationVersion | Write-Host
                }
                Write-EndProcessLine -TaskStartTime $SubTaskStartTime
            }


            # Unpublish the previous app if it is not installed in any tenant anymore.
            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Unpublishing previous app version.'  
                if ($SkipUnpublish -eq $false) {
                    if(Get-NAVAppTenant -ServerInstance $ServerInstance -Id $CurrentApp.AppId.Value -Version $CurrentAppVersion){
                        'Previous app {0} version {1} is not unpublished because it is still in use by another tenant.' -f 
                            $CurrentAppName, $CurrentAppVersion | Write-Host
                    } else {
                        'Unpublishing {0} App, version {1}' -f $CurrentAppName, $CurrentAppVersion | Write-Host
                        "Unpublish-NAVApp -ServerInstance '{0}' -Name '{1}' -Version '{2}'" -f
                            $ServerInstance, $CurrentAppName, $CurrentAppVersion | Write-Host

                        Unpublish-NAVApp `
                            -ServerInstance $ServerInstance `
                            -Name $CurrentAppName `
                            -Version $CurrentAppVersion
                    }
                } else {
                    'Skipped Unpublish-NAVApp.' | Write-Host 
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime


            # Install uninstalled childapps again if AutoInstallChildApps is true)
            $SubTaskStartTime = Write-StartProcessLine -StartLogText 'Reinstalling child-apps.'  
                if (-not $SkipAutoInstallChildApps -and $InstalledDependendApps.Count -ge 1 -and -not $SkipUnInstall){
                    'Installing ChildApps.' | Write-Host
                    
                    # Get the right installation order
                    $InstalledDependendApps = Get-AppDependencyOrder -Apps $InstalledDependendApps -ServerInstance $ServerInstance
                    
                    foreach ($ChildApp in $InstalledDependendApps){

                        # Skip the child-app installation if the app is included in the to-install apps,
                        # except if the to-install app is a lower version.
                        if ($ChildApp.AppId.Value.Guid -in $Apps.AppId.Value.Guid -and
                            $ChildApp.Version -le ($Apps | Where-Object {$_.AppId.Value.Guid -eq $ChildApp.AppId.Value.Guid}).Version
                        ){ 
                            ' Skipped install for child app {0} because the app is included in the to-install appfiles' -f $ChildApp.Name | Write-Host
                            continue
                        }
                        
                        if ($AppIsBaseApp){
                            ' Recompiling child app {0} against the new base app {1}.' -f $ChildApp.Name, $App.Name | Write-Host
                            Repair-NAVApp `
                                -ServerInstance $ServerInstance `
                                -Name $ChildApp.Name `
                                -Version $ChildApp.Version | Out-Null
                        }
                        ' Installing app {0}' -f $ChildApp.Name | Write-Host
                        Install-NAVApp `
                            -ServerInstance $ServerInstance `
                            -Name $ChildApp.Name `
                            -Version $ChildApp.Version `
                            -Tenant $Tenant `
                            -Force
                    }
                } else {
                    'Skipped installing child apps.' | Write-Host
                }
            Write-EndProcessLine -TaskStartTime $SubTaskStartTime
            'Total deploy time app {0} for tenant {1}:' -f $AppName, $Tenant | Write-Host
            Write-EndProcessLine -TaskStartTime $AppDeployStartTime
        } # End foreach tenant
    } # End foreach app file

    'Total deploy time Business Central Extension(s):' | Write-Host
    Write-EndProcessLine -TaskStartTime $TaskStartTime


    $TaskStartTime = Write-StartProcessLine -StartLogText ('Restarting ServerInstance {0}.' -f $ServerInstance)
        if($SkipServiceRestart -eq $false){
            Set-NavServerInstance `
                -ServerInstance $ServerInstance `
                -Restart
            Wait-BcServerInstanceMountingTenants `
                -ServerInstance $ServerInstance
        } else {
            'Skipped restarting ServiceInstance because switch SkipServiceRestart is set.'
        }
    Write-EndProcessLine -TaskStartTime $TaskStartTime


    $TaskStartTime = Write-StartProcessLine -StartLogText ('Writing summary.')
    $Tenants | ForEach-Object {
        $Message = (Get-NavAppInfo `
                        -ServerInstance $ServerInstance `
                        -Tenant $_ `
                        -TenantSpecificProperties | Sort-Object -Property Name, Version | `
                            Select-Object -Property Name, Publisher, IsPublished, SyncState, NeedsUpgrade, IsInstalled, ExtensionDataVersion, Version, AppId | `
                            Format-Table | Out-String)
        Write-host (Write-DebugInfoToOutput -MessageTitle ('Extension states for tenant {0}' -f $_) -MessageBody $Message)
    }           
    Write-EndProcessLine -TaskStartTime $TaskStartTime

    'Total execution time:' | Write-Host
    Write-EndProcessLine -TaskStartTime $TotalTime

    'Installation completed.' | Write-Host

    try{ Stop-Transcript }catch{}
}

Export-ModuleMember -Function Install-BcApp