/// <summary>
        /// Returns true if all checks are passed to permit a tenant to see an app.
        /// </summary>
        /// <param name="installedApp">Entry from tenant, or null if not in tenant</param>
        /// <param name="availableApp">Entry from app-library, or null if not in library</param>
        /// <param name="securityModel">Security method to apply.</param>
        public bool CanSee(InstalledApplication installedApp, AvailableApplication availableApp, AppSecurityModel securityModel)
        {
            Guid systemSolution = new Guid("3e67c1c4-aa65-4a9f-95d2-908a9f3614d1");

            if (availableApp?.ApplicationId == systemSolution)
            {
                return(false);
            }

            bool isAvailable = !string.IsNullOrWhiteSpace(availableApp?.PackageVersion);
            bool isInstalled = !string.IsNullOrWhiteSpace(installedApp?.SolutionVersion);

            if (!isInstalled && !isAvailable)
            {
                return(false);
            }

            switch (securityModel)
            {
            case AppSecurityModel.Restricted:
                return(isInstalled);

            case AppSecurityModel.Full:
                return(true);

            case AppSecurityModel.PerTenant:
                return(isInstalled || availableApp.HasInstallPermission);

            default:
                throw new InvalidOperationException("Unknown application security setting");
            }
        }
        /// <summary>
        /// Returns true if all checks are passed to permit an app to be repaired.
        /// </summary>
        /// <param name="installedApp">Entry from tenant, or null if not in tenant</param>
        /// <param name="availableApp">Entry from app-library, or null if not in library</param>
        /// <param name="securityModel">Security method to apply.</param>
        public bool CanRepair(InstalledApplication installedApp, AvailableApplication availableApp, AppSecurityModel securityModel)
        {
            bool isAvailable = !string.IsNullOrWhiteSpace(availableApp?.PackageVersion);
            bool isInstalled = !string.IsNullOrWhiteSpace(installedApp?.SolutionVersion);

            if (!(isInstalled && isAvailable))
            {
                return(false);
            }

            bool possibleToRepair = AppManager.CanRepair(installedApp.SolutionVersion, availableApp.PackageVersion);

            if (!possibleToRepair)
            {
                return(false);
            }

            switch (securityModel)
            {
            case AppSecurityModel.Restricted:
            case AppSecurityModel.Full:
                return(true);

            case AppSecurityModel.PerTenant:
                return(availableApp.HasInstallPermission);

            default:
                throw new InvalidOperationException("Unknown application security setting");
            }
        }
        /// <summary>
        /// Returns true if all checks are passed to permit an app to be deployed.
        /// </summary>
        /// <param name="installedApp">Entry from tenant, or null if not in tenant</param>
        /// <param name="availableApp">Entry from app-library, or null if not in library</param>
        /// <param name="securityModel">Security method to apply.</param>
        public bool CanDeploy(InstalledApplication installedApp, AvailableApplication availableApp, AppSecurityModel securityModel)
        {
            bool isAvailable = !string.IsNullOrWhiteSpace(availableApp?.PackageVersion);
            bool isInstalled = !string.IsNullOrWhiteSpace(installedApp?.SolutionVersion);

            if (isInstalled)
            {
                return(false);
            }
            if (!isAvailable)
            {
                return(false);
            }

            switch (securityModel)
            {
            case AppSecurityModel.Restricted:
                return(false);

            case AppSecurityModel.Full:
                return(true);

            case AppSecurityModel.PerTenant:
                return(availableApp.HasInstallPermission);

            default:
                throw new InvalidOperationException("Unknown application security setting");
            }
        }
        /// <summary>
        /// Returns true if all checks are passed to permit an app to be published.
        /// </summary>
        /// <param name="installedApp">Entry from tenant, or null if not in tenant</param>
        /// <param name="availableApp">Entry from app-library, or null if not in library</param>
        /// <param name="securityModel">Security method to apply.</param>
        public bool CanPublish(InstalledApplication installedApp, AvailableApplication availableApp, AppSecurityModel securityModel)
        {
            bool isAvailable = !string.IsNullOrWhiteSpace(availableApp?.PackageVersion);
            bool isInstalled = !string.IsNullOrWhiteSpace(installedApp?.SolutionVersion);

            if (!isInstalled)
            {
                return(false);
            }
            if (!isAvailable)
            {
                return(true);        // todo: add special permission for first-time publish of new apps
            }
            bool somethingToPublish = AppManager.CanPublish(installedApp.SolutionVersion, availableApp.PackageVersion);

            if (!somethingToPublish)
            {
                return(false);
            }

            switch (securityModel)
            {
            case AppSecurityModel.Restricted:
                return(false);

            case AppSecurityModel.Full:
                return(true);

            case AppSecurityModel.PerTenant:
                return(availableApp.HasPublishPermission);

            default:
                throw new InvalidOperationException("Unknown application security setting");
            }
        }
        /// <summary>
        /// Helper function to load application records and validate if OK to proceed with an operation.
        /// </summary>
        /// <param name="applicationId">The application Guid. (Not the package guid)</param>
        /// <param name="validationFunction">Validation callback.</param>
        /// <returns>True if we can proceed.</returns>
        private bool CheckIfPossible(Guid applicationId, Func <InstalledApplication, AvailableApplication, AppSecurityModel, bool> validationFunction)
        {
            if (validationFunction == null)
            {
                throw new ArgumentNullException(nameof(validationFunction));
            }

            // Get application security setting
            var security = ApplicationSecuritySettings.Current == null
                ? AppSecurityModel.Restricted
                : ApplicationSecuritySettings.Current.AppSecurityModel;

            // Find application records
            InstalledApplication installedApp = GetInstalledApplications(applicationId).SingleOrDefault( );
            AvailableApplication availableApp = GetAvailableApplications(applicationId).SingleOrDefault( );

            bool result = validationFunction(installedApp, availableApp, security);

            return(result);
        }
        /// <summary>
        /// Returns true if all checks are passed to permit an app to be exported.
        /// </summary>
        /// <param name="installedApp">Entry from tenant, or null if not in tenant</param>
        /// <param name="availableApp">Entry from app-library, or null if not in library</param>
        /// <param name="securityModel">Security method to apply.</param>
        public bool CanExport(InstalledApplication installedApp, AvailableApplication availableApp, AppSecurityModel securityModel)
        {
            bool isAvailable = availableApp != null;

            if (!isAvailable)
            {
                return(false);
            }

            switch (securityModel)
            {
            case AppSecurityModel.Restricted:
                return(false);

            case AppSecurityModel.Full:
                return(true);

            case AppSecurityModel.PerTenant:
                return(availableApp.HasPublishPermission);    // can probably improve this

            default:
                throw new InvalidOperationException("Unknown application security setting");
            }
        }
        /// <summary>
        ///     Gets the installed applications.
        /// </summary>
        /// <param name="applicationId">Optionally filter results to this ID only.</param>
        /// <returns></returns>
        public IList <InstalledApplication> GetInstalledApplications(Guid?applicationId = null)
        {
            var applications = new List <InstalledApplication>( );

            using (DatabaseContext ctx = DatabaseContext.GetContext( ))
            {
                using (IDbCommand command = ctx.CreateCommand( ))
                {
                    const string sql = @"-- GetInstalledApplications
DECLARE @name BIGINT = dbo.fnAliasNsId( 'name', 'core', DEFAULT )
DECLARE @appVersionString BIGINT = dbo.fnAliasNsId( 'appVersionString', 'core', DEFAULT )
DECLARE @appVerId BIGINT = dbo.fnAliasNsId( 'appVerId', 'core', DEFAULT )
DECLARE @packageForApplication BIGINT = dbo.fnAliasNsId( 'packageForApplication', 'core', DEFAULT )
DECLARE @solutionVersionString BIGINT = dbo.fnAliasNsId( 'solutionVersionString', 'core', @tenantId )
DECLARE @isOfType BIGINT = dbo.fnAliasNsId( 'isOfType', 'core', @tenantId )
DECLARE @solution BIGINT = dbo.fnAliasNsId( 'solution', 'core', @tenantId )

SELECT
	Solution = sn.Data,
    SolutionEntityId = s.FromId,
    SolutionVersion = sv.Data,
    PackageId = pid.Data,
    PackageEntityId = p.EntityId,
    Version = pv.Data,
    ApplicationEntityId = pa.ToId,
    ApplicationId = aid.UpgradeId,
    Publisher = p1.Data,
    PublisherUrl = u.Data,
    ReleaseDate = c.Data
FROM
	Relationship s   -- isOfType solution
JOIN
	Data_NVarChar sv ON s.FromId = sv.EntityId
	AND sv.FieldId = @solutionVersionString
	AND sv.TenantId = @tenantId
CROSS APPLY
	dbo.tblFnFieldNVarCharA( s.FromId, s.TenantId, 'name', 'core' ) sn
CROSS APPLY
	dbo.tblFnFieldGuidA( s.FromId, s.TenantId, 'packageId', 'core' ) pid
LEFT JOIN
	Data_Guid p ON pid.Data = p.Data
	AND p.FieldId = @appVerId
	AND p.TenantId = 0
LEFT JOIN
	Relationship pa ON p.EntityId = pa.FromId
	AND pa.TypeId = @packageForApplication
	AND pa.TenantId = 0
LEFT JOIN
	Data_NVarChar pv ON p.EntityId = pv.EntityId
	AND pv.FieldId = @appVersionString
	AND pv.TenantId = 0
OUTER APPLY
	dbo.tblFnFieldNVarCharA( s.FromId, s.TenantId, 'solutionPublisher', 'core' ) p1
OUTER APPLY
	dbo.tblFnFieldNVarCharA( s.FromId, s.TenantId, 'solutionPublisherUrl', 'core' ) u
OUTER APPLY
	dbo.tblFnFieldDateTimeA( s.FromId, s.TenantId, 'solutionReleaseDate', 'core' ) c
JOIN
	Entity aid ON s.FromId = aid.Id AND s.TenantId = aid.TenantId
WHERE
	s.TenantId = @tenantId
	AND s.TypeId = @isOfType
	AND s.ToId = @solution
	AND
	(
		p.EntityId = pa.FromId
		OR
		p.EntityId IS NULL
	)
    AND
    (
        @applicationGuid = convert(uniqueidentifier, '00000000-0000-0000-0000-000000000000')
        OR
        @applicationGuid = aid.UpgradeId
    )
ORDER BY
	sn.Data, pv.Data"    ;

                    command.CommandText = sql;

                    ctx.AddParameter(command, "@tenantId", DbType.Int64, RequestContext.TenantId);
                    ctx.AddParameter(command, "@applicationGuid", DbType.Guid, applicationId ?? Guid.Empty);

                    using (IDataReader reader = command.ExecuteReader( ))
                    {
                        while (reader.Read( ))
                        {
                            var application = new InstalledApplication
                            {
                                Name                 = reader.GetString(0),
                                SolutionEntityId     = reader.GetInt64(1),
                                SolutionVersion      = reader.GetString(2),
                                ApplicationVersionId = reader.GetGuid(3),
                                PackageEntityId      = reader.GetInt64(4, -1),
                                PackageVersion       = reader.GetString(5, null),
                                ApplicationEntityId  = reader.GetInt64(6, -1),
                                ApplicationId        = reader.GetGuid(7, Guid.Empty),
                                Publisher            = reader.GetString(8, null),
                                PublisherUrl         = reader.GetString(9, null),
                                ReleaseDate          = reader.GetDateTime(10, DateTime.MinValue)
                            };

                            applications.Add(application);
                        }
                    }
                }
            }

            return(applications.GroupBy(a => a.ApplicationId).Select(a => a.OrderByDescending(x => x.PackageEntityId).First( )).ToList( ));
        }