diff --git a/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj b/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj
index 5aa7f8d6..65aa6dd3 100644
--- a/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj
+++ b/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj
@@ -34,10 +34,10 @@
-
-
-
-
+
+
+
+
@@ -45,7 +45,7 @@
-
+
diff --git a/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj b/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj
index c509a130..6295dd52 100644
--- a/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj
+++ b/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj b/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj
index b4bfd8ab..8a9ab48c 100644
--- a/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj
+++ b/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj
@@ -34,8 +34,8 @@
-
-
-
+
+
+
diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
index 071b1792..ae4f26ff 100644
--- a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
+++ b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
@@ -66,7 +66,7 @@ private static async Task SetUpNewTenantAndGoToFeaturesListAsync(
{
await context.SignInDirectlyAsync();
- await context.CreateAndSwitchToTenantManuallyAsync(tenantName, tenantUrlPrefix, string.Empty, "features guard");
+ await context.CreateAndSwitchToTenantManuallyAsync(tenantName, tenantUrlPrefix, string.Empty, "Features Guard");
await context.GoToSetupPageAndSetupOrchardCoreAsync(
new OrchardCoreSetupParameters(context)
diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj
index da830ffc..078102de 100644
--- a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj
+++ b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj
@@ -27,13 +27,13 @@
-
+
-
+
diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs b/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs
index 3da2235a..e9b6682e 100644
--- a/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs
+++ b/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs
@@ -1,10 +1,9 @@
using Lombiq.Hosting.Tenants.FeaturesGuard.Models;
-using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OrchardCore.Environment.Extensions.Features;
using OrchardCore.Environment.Shell;
-using OrchardCore.Environment.Shell.Descriptor;
-using OrchardCore.Environment.Shell.Descriptor.Models;
+using OrchardCore.Environment.Shell.Scope;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,168 +13,145 @@ namespace Lombiq.Hosting.Tenants.FeaturesGuard.Handlers;
public sealed class FeaturesEventHandler : IFeatureEventHandler
{
- private readonly IShellFeaturesManager _shellFeaturesManager;
- private readonly IOptions _conditionallyEnabledFeaturesOptions;
- private readonly ShellSettings _shellSettings;
- private readonly IShellDescriptorManager _shellDescriptorManager;
-
- public FeaturesEventHandler(
- IShellFeaturesManager shellFeaturesManager,
- IOptions conditionallyEnabledFeaturesOptions,
- ShellSettings shellSettings,
- IConfiguration configuration,
- IShellDescriptorManager shellDescriptorManager)
- {
- _shellFeaturesManager = shellFeaturesManager;
- _conditionallyEnabledFeaturesOptions = conditionallyEnabledFeaturesOptions;
- _shellSettings = shellSettings;
- _shellDescriptorManager = shellDescriptorManager;
- }
-
- Task IFeatureEventHandler.InstallingAsync(IFeatureInfo feature) => Task.CompletedTask;
-
- Task IFeatureEventHandler.InstalledAsync(IFeatureInfo feature) => Task.CompletedTask;
-
- Task IFeatureEventHandler.EnablingAsync(IFeatureInfo feature) => Task.CompletedTask;
-
- Task IFeatureEventHandler.EnabledAsync(IFeatureInfo feature) => EnableConditionallyEnabledFeaturesAsync(feature);
-
- Task IFeatureEventHandler.DisablingAsync(IFeatureInfo feature) => Task.CompletedTask;
-
- async Task IFeatureEventHandler.DisabledAsync(IFeatureInfo feature)
- {
- await KeepConditionallyEnabledFeaturesEnabledAsync(feature);
- await DisableConditionallyEnabledFeaturesAsync(feature);
- }
-
- Task IFeatureEventHandler.UninstallingAsync(IFeatureInfo feature) => Task.CompletedTask; // #spell-check-ignore-line
+ private bool _deferredTaskTriggered;
- Task IFeatureEventHandler.UninstalledAsync(IFeatureInfo feature) => Task.CompletedTask;
+ public Task InstallingAsync(IFeatureInfo feature) => Task.CompletedTask;
- ///
- /// Enables conditional features (key) if one of their corresponding condition features (value) was enabled.
- ///
- /// The feature that was just enabled.
- public async Task EnableConditionallyEnabledFeaturesAsync(IFeatureInfo featureInfo)
- {
- if (_shellSettings.IsDefaultShell() ||
- _conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures)
- {
- return;
- }
-
- var allConditionFeatureIds = new List();
- foreach (var conditionFeatureIds in conditionallyEnabledFeatures.Values)
- {
- allConditionFeatureIds.AddRange(conditionFeatureIds);
- }
+ public Task InstalledAsync(IFeatureInfo feature) => Task.CompletedTask;
- if (!allConditionFeatureIds.Contains(featureInfo.Id))
- {
- return;
- }
+ public Task EnablingAsync(IFeatureInfo feature) => Task.CompletedTask;
- // Enable conditional features if they are not already enabled.
- var allFeatures = await _shellFeaturesManager.GetAvailableFeaturesAsync();
+ public Task EnabledAsync(IFeatureInfo feature) => HandleConditionallyEnabledFeaturesAsync();
- var conditionalFeatureIds = conditionallyEnabledFeatures
- .Where(keyValuePair => keyValuePair.Value.Contains(featureInfo.Id))
- .Select(keyValuePair => keyValuePair.Key)
- .ToList();
+ public Task DisablingAsync(IFeatureInfo feature) => Task.CompletedTask;
- // During setup, Shell Descriptor can become out of sync with the DB when it comes to enabled features,
- // but it's more accurate than IShellDescriptorManager's methods.
- var shellDescriptor = await _shellDescriptorManager.GetShellDescriptorAsync();
+ public Task DisabledAsync(IFeatureInfo feature) => HandleConditionallyEnabledFeaturesAsync();
- // If Shell Descriptor's Features already contains a feature that is found in conditionalFeatures, remove it
- // from the list. Handle multiple conditional features as well.
- var featuresToEnable = allFeatures.Where(feature =>
- conditionalFeatureIds.Contains(feature.Id) && !shellDescriptor.Features.Contains(new ShellFeature(feature.Id)));
+ public Task UninstallingAsync(IFeatureInfo feature) => Task.CompletedTask; // #spell-check-ignore-line
- await _shellFeaturesManager.EnableFeaturesAsync(featuresToEnable, force: true);
- }
+ public Task UninstalledAsync(IFeatureInfo feature) => Task.CompletedTask;
///
- /// When a conditional feature (key) is disabled, keeps the conditional feature enabled if any of the corresponding
- /// condition features (value) are enabled.
+ /// Enables or disables conditional features depending on ConditionallyEnabledFeaturesOptions.
+ /// Prevents disabling features that should be enabled according to their conditions.
///
- /// The feature that was just disabled.
- public async Task KeepConditionallyEnabledFeaturesEnabledAsync(IFeatureInfo featureInfo)
+ private Task HandleConditionallyEnabledFeaturesAsync()
{
- if (_shellSettings.IsDefaultShell() ||
- _conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures)
+ if (_deferredTaskTriggered)
{
- return;
+ return Task.CompletedTask;
}
- if (!conditionallyEnabledFeatures.ContainsKey(featureInfo.Id))
- {
- return;
- }
-
- // Re-enable conditional feature if any its condition features are enabled.
- var allFeatures = await _shellFeaturesManager.GetAvailableFeaturesAsync();
- var conditionFeatureIds = conditionallyEnabledFeatures[featureInfo.Id];
+ _deferredTaskTriggered = true;
- var currentlyEnabledFeatures = await _shellFeaturesManager.GetEnabledFeaturesAsync();
- var conditionFeatures = allFeatures.Where(feature => conditionFeatureIds.Contains(feature.Id));
-
- var currentlyEnabledConditionFeatures = currentlyEnabledFeatures.Intersect(conditionFeatures);
- if (currentlyEnabledConditionFeatures.Any())
+ ShellScope.AddDeferredTask(async scope =>
{
- var conditionalFeature = allFeatures.Where(feature => feature.Id == featureInfo.Id);
- await _shellFeaturesManager.EnableFeaturesAsync(conditionalFeature);
- }
+ if (scope.ShellContext.Settings.IsDefaultShell())
+ {
+ return;
+ }
+
+ var shellFeaturesManager = scope
+ .ServiceProvider
+ .GetRequiredService();
+
+ var conditionallyEnabledFeaturesOptions = scope
+ .ServiceProvider
+ .GetRequiredService>()
+ .Value
+ .EnableFeatureIfOtherFeatureIsEnabled;
+
+ var enabledFeatures = (await shellFeaturesManager.GetEnabledFeaturesAsync())
+ .ToHashSet();
+
+ var enabledFeaturesIds = enabledFeatures
+ .Select(feature => feature.Id)
+ .ToHashSet();
+
+ if (!TryGetFeaturesToBeEnabledAndDisabled(
+ conditionallyEnabledFeaturesOptions,
+ enabledFeaturesIds,
+ out var featuresToEnableIds,
+ out var featuresToDisableIds))
+ {
+ return;
+ }
+
+ var availableFeatures = await shellFeaturesManager.GetAvailableFeaturesAsync();
+
+ var featuresToEnable = availableFeatures
+ .Where(feature => featuresToEnableIds.Contains(feature.Id))
+ .ToList();
+
+ if (featuresToEnable.Exists(feature => feature.DefaultTenantOnly || feature.EnabledByDependencyOnly))
+ {
+ throw new InvalidOperationException("'DefaultTenantOnly' feature can't be enabled by FeaturesGuard.");
+ }
+
+ var featuresToDisable = enabledFeatures
+ .Where(feature => featuresToDisableIds.Contains(feature.Id))
+ .ToList();
+
+ if (featuresToDisable.Exists(feature => feature.IsAlwaysEnabled || feature.EnabledByDependencyOnly))
+ {
+ throw new InvalidOperationException("'IsAlwaysEnabled' feature can't be disabled by FeaturesGuard.");
+ }
+
+ if (!featuresToEnable.Any() && !featuresToDisable.Any())
+ {
+ return;
+ }
+
+ await shellFeaturesManager.UpdateFeaturesAsync(featuresToDisable, featuresToEnable, force: true);
+ });
+
+ return Task.CompletedTask;
}
///
- /// When a condition feature (value) is disabled, disables the corresponding conditional features (key) if all of
- /// their condition features are disabled.
+ /// Extracts the feature ids from ConditionallyEnabledFeaturesOptions and separates them into
+ /// and hash sets
+ /// and compares them to collection to determine which features need to be
+ /// enabled or disabled.
///
- /// The feature that was just disabled.
- public async Task DisableConditionallyEnabledFeaturesAsync(IFeatureInfo featureInfo)
+ /// A boolean value whether ConditionallyEnabledFeaturesOptions is populated or not.
+ /// Also produces and .
+ ///
+ private static bool TryGetFeaturesToBeEnabledAndDisabled(
+ IDictionary> conditionallyEnabledFeatures,
+ IReadOnlySet enabledFeatureIds,
+ out HashSet featuresToEnable,
+ out HashSet featuresToDisable)
{
- if (_shellSettings.IsDefaultShell() ||
- _conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures)
+ if (!conditionallyEnabledFeatures.Any())
{
- return;
+ featuresToEnable = null;
+ featuresToDisable = null;
+ return false;
}
- var allConditionFeatureIds = new List();
- foreach (var conditionFeatureIdList in conditionallyEnabledFeatures.Values)
- {
- allConditionFeatureIds.AddRange(conditionFeatureIdList);
- }
+ var featuresToEnableIds = new HashSet();
+ var featuresToDisableIds = new HashSet();
- if (!allConditionFeatureIds.Contains(featureInfo.Id))
+ foreach (var condition in conditionallyEnabledFeatures)
{
- return;
+ var hasConditional = enabledFeatureIds.Contains(condition.Key);
+ var hasCondition = enabledFeatureIds.Intersect(condition.Value).Any();
+
+ if (hasCondition && !hasConditional)
+ {
+ featuresToEnableIds.Add(condition.Key);
+ }
+
+ if (!hasCondition && hasConditional)
+ {
+ featuresToDisableIds.Add(condition.Key);
+ }
}
- // If current feature is one of the condition features, disable its corresponding conditional features if they
- // are not already disabled.
- var allFeatures = await _shellFeaturesManager.GetAvailableFeaturesAsync();
-
- var conditionalFeatureIds = new List();
- var conditionFeatureIds = new List();
- foreach (var keyValuePair in conditionallyEnabledFeatures.Where(keyValuePair => keyValuePair.Value.Contains(featureInfo.Id)))
- {
- conditionalFeatureIds.Add(keyValuePair.Key);
- conditionFeatureIds.AddRange(keyValuePair.Value);
- }
+ featuresToEnable = featuresToEnableIds;
+ featuresToDisable = featuresToDisableIds;
- var currentlyEnabledFeatures = await _shellFeaturesManager.GetEnabledFeaturesAsync();
- var conditionFeatures = allFeatures.Where(feature => conditionFeatureIds.Contains(feature.Id));
-
- // Only disable conditional feature if none of its condition features are enabled.
- var currentlyEnabledConditionFeatures = currentlyEnabledFeatures.Intersect(conditionFeatures);
- if (!currentlyEnabledConditionFeatures.Any())
- {
- // Handle multiple conditional features as well.
- var conditionalFeatures = allFeatures.Where(feature => conditionalFeatureIds.Contains(feature.Id));
- var currentlyEnabledConditionalFeatures = currentlyEnabledFeatures.Intersect(conditionalFeatures);
-
- await _shellFeaturesManager.DisableFeaturesAsync(currentlyEnabledConditionalFeatures);
- }
+ return featuresToEnableIds.Any() || featuresToDisableIds.Any();
}
}
diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj b/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj
index b261ca10..9f722cdc 100644
--- a/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj
+++ b/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj
@@ -34,11 +34,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -47,8 +47,8 @@
-
-
+
+
diff --git a/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj b/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj
index 3c473d98..70bf4784 100644
--- a/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj
+++ b/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj b/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj
index b5ec9b2c..23d1dfa2 100644
--- a/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj
+++ b/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj
@@ -24,9 +24,9 @@
-
-
-
+
+
+
diff --git a/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj b/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj
index b6787a0c..f404b07a 100644
--- a/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj
+++ b/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj b/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj
index 303ab779..30e11eb1 100644
--- a/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj
+++ b/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj
@@ -23,13 +23,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -48,7 +48,7 @@
-
+
diff --git a/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj b/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj
index f97aa793..da73240a 100644
--- a/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj
+++ b/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj
@@ -34,10 +34,10 @@
-
-
-
-
+
+
+
+
@@ -45,7 +45,7 @@
-
+
diff --git a/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj b/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj
index 40863d93..6829289a 100644
--- a/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj
+++ b/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj
@@ -33,7 +33,7 @@
-
+
diff --git a/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj b/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj
index 335edbd9..fe67cfc0 100644
--- a/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj
+++ b/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj
@@ -34,10 +34,10 @@
-
-
-
-
+
+
+
+
@@ -46,7 +46,7 @@
-
+