Skip to content

Commit

Permalink
Merge pull request #55 from Lombiq/issue/OSOE-548
Browse files Browse the repository at this point in the history
OSOE-548: Upgrade to Orchard Core 1.6 in Hosting-Tenants
  • Loading branch information
Piedone authored Jun 18, 2023
2 parents ed10328 + c605bc8 commit a97b5ea
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 179 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.5.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.5.0" />
<PackageReference Include="OrchardCore.Tenants" Version="1.5.0" />
<PackageReference Include="OrchardCore.Users" Version="1.5.0" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.6.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.6.0" />
<PackageReference Include="OrchardCore.Tenants" Version="1.6.0" />
<PackageReference Include="OrchardCore.Users" Version="1.6.0" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' != 'true'">
<ProjectReference Include="..\..\..\Libraries\Lombiq.HelpfulLibraries\Lombiq.HelpfulLibraries.OrchardCore\Lombiq.HelpfulLibraries.OrchardCore.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.HelpfulLibraries.OrchardCore" Version="5.2.0" />
<PackageReference Include="Lombiq.HelpfulLibraries.OrchardCore" Version="6.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.Tests.UI" Version="6.1.0" />
<PackageReference Include="Lombiq.Tests.UI" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="OrchardCore.Infrastructure.Abstractions" Version="1.5.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.5.0" />
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.5.0" />
<PackageReference Include="OrchardCore.Infrastructure.Abstractions" Version="1.6.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.6.0" />
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.6.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
<None Remove="node_modules\**" />
<None Remove="Tests\**" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' != 'true'">
<ProjectReference Include="..\..\..\..\test\Lombiq.UITestingToolbox\Lombiq.Tests.UI\Lombiq.Tests.UI.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.Tests.UI" Version="6.1.0" />
<PackageReference Include="Lombiq.Tests.UI" Version="7.0.0" />
</ItemGroup>

</Project>
248 changes: 112 additions & 136 deletions Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,168 +13,145 @@ namespace Lombiq.Hosting.Tenants.FeaturesGuard.Handlers;

public sealed class FeaturesEventHandler : IFeatureEventHandler
{
private readonly IShellFeaturesManager _shellFeaturesManager;
private readonly IOptions<ConditionallyEnabledFeaturesOptions> _conditionallyEnabledFeaturesOptions;
private readonly ShellSettings _shellSettings;
private readonly IShellDescriptorManager _shellDescriptorManager;

public FeaturesEventHandler(
IShellFeaturesManager shellFeaturesManager,
IOptions<ConditionallyEnabledFeaturesOptions> 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;

/// <summary>
/// Enables conditional features (key) if one of their corresponding condition features (value) was enabled.
/// </summary>
/// <param name="featureInfo">The feature that was just enabled.</param>
public async Task EnableConditionallyEnabledFeaturesAsync(IFeatureInfo featureInfo)
{
if (_shellSettings.IsDefaultShell() ||
_conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures)
{
return;
}

var allConditionFeatureIds = new List<string>();
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;

/// <summary>
/// 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.
/// </summary>
/// <param name="featureInfo">The feature that was just disabled.</param>
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<IShellFeaturesManager>();

var conditionallyEnabledFeaturesOptions = scope
.ServiceProvider
.GetRequiredService<IOptions<ConditionallyEnabledFeaturesOptions>>()
.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;
}

/// <summary>
/// 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
/// <paramref name="featuresToEnable"></paramref> and <paramref name="featuresToDisable"></paramref> hash sets
/// and compares them to <paramref name="enabledFeatureIds"/> collection to determine which features need to be
/// enabled or disabled.
/// </summary>
/// <param name="featureInfo">The feature that was just disabled.</param>
public async Task DisableConditionallyEnabledFeaturesAsync(IFeatureInfo featureInfo)
/// <returns>A boolean value whether ConditionallyEnabledFeaturesOptions is populated or not.
/// Also produces <paramref name="featuresToEnable"/> and <paramref name="featuresToDisable"/>.
/// </returns>
private static bool TryGetFeaturesToBeEnabledAndDisabled(
IDictionary<string, IEnumerable<string>> conditionallyEnabledFeatures,
IReadOnlySet<string> enabledFeatureIds,
out HashSet<string> featuresToEnable,
out HashSet<string> featuresToDisable)
{
if (_shellSettings.IsDefaultShell() ||
_conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures)
if (!conditionallyEnabledFeatures.Any())
{
return;
featuresToEnable = null;
featuresToDisable = null;
return false;
}

var allConditionFeatureIds = new List<string>();
foreach (var conditionFeatureIdList in conditionallyEnabledFeatures.Values)
{
allConditionFeatureIds.AddRange(conditionFeatureIdList);
}
var featuresToEnableIds = new HashSet<string>();
var featuresToDisableIds = new HashSet<string>();

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<string>();
var conditionFeatureIds = new List<string>();
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="OrchardCore.Infrastructure.Abstractions" Version="1.5.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.5.0" />
<PackageReference Include="OrchardCore.ContentManagement" Version="1.5.0" />
<PackageReference Include="OrchardCore.ContentTypes.Abstractions" Version="1.5.0" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.5.0" />
<PackageReference Include="OrchardCore.Infrastructure.Abstractions" Version="1.6.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.6.0" />
<PackageReference Include="OrchardCore.ContentManagement" Version="1.6.0" />
<PackageReference Include="OrchardCore.ContentTypes.Abstractions" Version="1.6.0" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.6.0" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' != 'true'">
Expand All @@ -47,8 +47,8 @@
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.HelpfulLibraries.OrchardCore" Version="5.2.0" />
<PackageReference Include="Lombiq.HelpfulLibraries.Common" Version="5.2.0" />
<PackageReference Include="Lombiq.HelpfulLibraries.OrchardCore" Version="6.0.0" />
<PackageReference Include="Lombiq.HelpfulLibraries.Common" Version="6.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.Tests.UI" Version="6.1.0" />
<PackageReference Include="Lombiq.Tests.UI" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit a97b5ea

Please sign in to comment.