diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/AbpAspNetCoreMvcUiThemeSharedModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/AbpAspNetCoreMvcUiThemeSharedModule.cs index b62d3f27ba..da418c8dc2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/AbpAspNetCoreMvcUiThemeSharedModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/AbpAspNetCoreMvcUiThemeSharedModule.cs @@ -4,6 +4,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Packages; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; +using Volo.Abp.Features; using Volo.Abp.Modularity; using Volo.Abp.VirtualFileSystem; @@ -12,7 +13,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; [DependsOn( typeof(AbpAspNetCoreMvcUiBootstrapModule), typeof(AbpAspNetCoreMvcUiPackagesModule), - typeof(AbpAspNetCoreMvcUiWidgetsModule) + typeof(AbpAspNetCoreMvcUiWidgetsModule), + typeof(AbpFeaturesModule) )] public class AbpAspNetCoreMvcUiThemeSharedModule : AbpModule { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Toolbars/ToolbarManager.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Toolbars/ToolbarManager.cs index 7822277371..c4e25c98a9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Toolbars/ToolbarManager.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Toolbars/ToolbarManager.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Mvc.UI.Theming; using Volo.Abp.Authorization.Permissions; using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; using Volo.Abp.SimpleStateChecking; namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; @@ -37,6 +38,7 @@ public class ToolbarManager : IToolbarManager, ITransientDependency using (var scope = ServiceProvider.CreateScope()) { using (RequirePermissionsSimpleBatchStateChecker.Use(new RequirePermissionsSimpleBatchStateChecker())) + using (RequireFeaturesSimpleBatchStateChecker.Use(new RequireFeaturesSimpleBatchStateChecker())) { var context = new ToolbarConfigurationContext(ThemeManager.CurrentTheme, toolbar, scope.ServiceProvider); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj index f80e51eb05..c2bc439691 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj @@ -30,6 +30,7 @@ + diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerBase.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerBase.cs index f17213af2a..34c01820b3 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerBase.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerBase.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -28,4 +29,16 @@ public abstract class FeatureCheckerBase : IFeatureChecker, ITransientDependency ); } } + + public virtual async Task> IsEnabledAsync(string[] names) + { + var result = new Dictionary(); + + foreach (var name in names) + { + result[name] = await IsEnabledAsync(name); + } + + return result; + } } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureSimpleStateCheckerExtensions.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureSimpleStateCheckerExtensions.cs index ff1bca8229..2c62c29870 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureSimpleStateCheckerExtensions.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureSimpleStateCheckerExtensions.cs @@ -1,4 +1,4 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; using Volo.Abp.SimpleStateChecking; namespace Volo.Abp.Features; @@ -10,7 +10,7 @@ public static class FeatureSimpleStateCheckerExtensions params string[] features) where TState : IHasSimpleStateCheckers { - state.RequireFeatures(true, features); + state.RequireFeatures(requiresAll: true, batchCheck: true, features); return state; } @@ -19,11 +19,32 @@ public static class FeatureSimpleStateCheckerExtensions bool requiresAll, params string[] features) where TState : IHasSimpleStateCheckers + { + state.RequireFeatures(requiresAll: requiresAll, batchCheck: true, features); + return state; + } + + public static TState RequireFeatures( + [NotNull] this TState state, + bool requiresAll, + bool batchCheck, + params string[] features) + where TState : IHasSimpleStateCheckers { Check.NotNull(state, nameof(state)); Check.NotNullOrEmpty(features, nameof(features)); - state.StateCheckers.Add(new RequireFeaturesSimpleStateChecker(requiresAll, features)); + if (batchCheck) + { + RequireFeaturesSimpleBatchStateChecker.Current.AddCheckModels( + new RequireFeaturesSimpleBatchStateCheckerModel(state, features, requiresAll)); + state.StateCheckers.Add(RequireFeaturesSimpleBatchStateChecker.Current); + } + else + { + state.StateCheckers.Add(new RequireFeaturesSimpleStateChecker(requiresAll, features)); + } + return state; } } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs index c604cb75b4..5f652269b5 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Text.Json.Nodes; using Volo.Abp.DependencyInjection; using Volo.Abp.SimpleStateChecking; @@ -10,7 +10,7 @@ public class FeaturesSimpleStateCheckerSerializerContributor : ISingletonDependency { public const string CheckerShortName = "F"; - + public string? SerializeToJson(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers { @@ -53,4 +53,4 @@ public class FeaturesSimpleStateCheckerSerializerContributor : nameArray.Select(x => x!.ToString()).ToArray() ); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs index 501c5ff8f8..f97b4504a0 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System.Collections.Generic; +using JetBrains.Annotations; using System.Threading.Tasks; namespace Volo.Abp.Features; @@ -8,4 +9,6 @@ public interface IFeatureChecker Task GetOrNullAsync([NotNull] string name); Task IsEnabledAsync(string name); + + Task> IsEnabledAsync([NotNull] string[] names); } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs new file mode 100644 index 0000000000..2cff28cc08 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.SimpleStateChecking; + +namespace Volo.Abp.Features; + +public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCheckerBase + where TState : IHasSimpleStateCheckers +{ + public static RequireFeaturesSimpleBatchStateChecker Current => _current.Value!; + private static readonly AsyncLocal> _current = new(); + + private readonly List> _models; + + static RequireFeaturesSimpleBatchStateChecker() + { + _current.Value = new RequireFeaturesSimpleBatchStateChecker(); + } + + public RequireFeaturesSimpleBatchStateChecker() + { + _models = new List>(); + } + + public RequireFeaturesSimpleBatchStateChecker AddCheckModels( + params RequireFeaturesSimpleBatchStateCheckerModel[] models) + { + Check.NotNullOrEmpty(models, nameof(models)); + + _models.AddRange(models); + return this; + } + + public static IDisposable Use(RequireFeaturesSimpleBatchStateChecker checker) + { + var previousValue = Current; + _current.Value = checker; + return new DisposeAction(() => _current.Value = previousValue); + } + + public override async Task> IsEnabledAsync( + SimpleBatchStateCheckerContext context) + { + var featureChecker = context.ServiceProvider.GetRequiredService(); + + var result = new SimpleStateCheckerResult(context.States); + + var relevantModels = _models + .Where(x => context.States.Any(s => s.Equals(x.State))) + .ToList(); + + var features = relevantModels.SelectMany(x => x.FeatureNames).Distinct().ToArray(); + var featureValues = await featureChecker.IsEnabledAsync(features); + + foreach (var state in context.States) + { + var model = relevantModels.FirstOrDefault(x => x.State.Equals(state)); + if (model != null) + { + if (model.RequiresAll) + { + result[state] = model.FeatureNames.All(x => featureValues.TryGetValue(x, out var v) && v); + } + else + { + result[state] = model.FeatureNames.Any(x => featureValues.TryGetValue(x, out var v) && v); + } + } + } + + return result; + } +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateCheckerModel.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateCheckerModel.cs new file mode 100644 index 0000000000..389ebac3af --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateCheckerModel.cs @@ -0,0 +1,23 @@ +using Volo.Abp.SimpleStateChecking; + +namespace Volo.Abp.Features; + +public class RequireFeaturesSimpleBatchStateCheckerModel + where TState : IHasSimpleStateCheckers +{ + public TState State { get; } + + public string[] FeatureNames { get; } + + public bool RequiresAll { get; } + + public RequireFeaturesSimpleBatchStateCheckerModel(TState state, string[] featureNames, bool requiresAll = true) + { + Check.NotNull(state, nameof(state)); + Check.NotNullOrEmpty(featureNames, nameof(featureNames)); + + State = state; + FeatureNames = featureNames; + RequiresAll = requiresAll; + } +} diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj b/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj index c5bc49a2bb..c01959bb61 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj +++ b/framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj @@ -23,6 +23,7 @@ + diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/AbpUiNavigationModule.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/AbpUiNavigationModule.cs index 9074943396..70fb9d978d 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/AbpUiNavigationModule.cs +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/AbpUiNavigationModule.cs @@ -1,4 +1,5 @@ using Volo.Abp.Authorization; +using Volo.Abp.Features; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; @@ -7,7 +8,7 @@ using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.UI.Navigation; -[DependsOn(typeof(AbpUiModule), typeof(AbpAuthorizationModule), typeof(AbpMultiTenancyModule))] +[DependsOn(typeof(AbpUiModule), typeof(AbpAuthorizationModule), typeof(AbpFeaturesModule), typeof(AbpMultiTenancyModule))] public class AbpUiNavigationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs index 6334ea9918..711d2ed4de 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp.Authorization.Permissions; using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; using Volo.Abp.SimpleStateChecking; namespace Volo.Abp.UI.Navigation; @@ -82,6 +83,7 @@ public class MenuManager : IMenuManager, ITransientDependency using (var scope = ServiceScopeFactory.CreateScope()) { using (RequirePermissionsSimpleBatchStateChecker.Use(new RequirePermissionsSimpleBatchStateChecker())) + using (RequireFeaturesSimpleBatchStateChecker.Use(new RequireFeaturesSimpleBatchStateChecker())) { var context = new MenuConfigurationContext(menu, scope.ServiceProvider); diff --git a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs new file mode 100644 index 0000000000..2ea3f2916b --- /dev/null +++ b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.MultiTenancy; +using Volo.Abp.SimpleStateChecking; +using Xunit; + +namespace Volo.Abp.Features; + +public class RequireFeaturesSimpleBatchStateChecker_Tests : FeatureTestBase +{ + private readonly ISimpleStateCheckerManager _simpleStateCheckerManager; + private readonly ICurrentTenant _currentTenant; + + public RequireFeaturesSimpleBatchStateChecker_Tests() + { + _simpleStateCheckerManager = GetRequiredService>(); + _currentTenant = GetRequiredService(); + } + + [Fact] + public void Switch_Current_Checker_Test() + { + var checker = RequireFeaturesSimpleBatchStateChecker.Current; + checker.ShouldNotBeNull(); + + RequireFeaturesSimpleBatchStateChecker checker2 = null; + + using (RequireFeaturesSimpleBatchStateChecker.Use(new RequireFeaturesSimpleBatchStateChecker())) + { + checker2 = RequireFeaturesSimpleBatchStateChecker.Current; + checker2.ShouldNotBeNull(); + checker2.ShouldNotBe(checker); + } + + checker2.ShouldNotBeNull(); + checker2.ShouldNotBe(checker); + } + + [Fact] + public async Task RequireFeaturesSimpleBatchStateChecker_Test() + { + // Tenant1: BooleanTestFeature1=true, BooleanTestFeature2=true + // Tenant2: no boolean features set → false + using (_currentTenant.Change(TestFeatureStore.Tenant1Id)) + { + var myStateEntities = new MyStateEntity[] + { + new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1"), + new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature2"), + new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), + new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), + }; + + var result = await _simpleStateCheckerManager.IsEnabledAsync(myStateEntities); + + result.Count.ShouldBe(myStateEntities.Length); + + result[myStateEntities[0]].ShouldBeTrue(); + result[myStateEntities[1]].ShouldBeTrue(); + result[myStateEntities[2]].ShouldBeTrue(); + result[myStateEntities[3]].ShouldBeTrue(); + } + + using (_currentTenant.Change(TestFeatureStore.Tenant2Id)) + { + var myStateEntities = new MyStateEntity[] + { + new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1"), + new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature2"), + new MyStateEntity().RequireFeatures(requiresAll: true, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), + new MyStateEntity().RequireFeatures(requiresAll: false, batchCheck: true, "BooleanTestFeature1", "BooleanTestFeature2"), + }; + + var result = await _simpleStateCheckerManager.IsEnabledAsync(myStateEntities); + + result.Count.ShouldBe(myStateEntities.Length); + + result[myStateEntities[0]].ShouldBeFalse(); + result[myStateEntities[1]].ShouldBeFalse(); + result[myStateEntities[2]].ShouldBeFalse(); + result[myStateEntities[3]].ShouldBeFalse(); + } + } + + class MyStateEntity : IHasSimpleStateCheckers + { + public List> StateCheckers { get; } + + public MyStateEntity() + { + StateCheckers = new List>(); + } + } + + class MyStateEntity2 : IHasSimpleStateCheckers + { + public List> StateCheckers { get; } + + public MyStateEntity2() + { + StateCheckers = new List>(); + } + } +} diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs index 4dca621a63..a8bdc4b4ea 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs @@ -57,7 +57,7 @@ public class PermissionDefinitionSerializer_Tests : PermissionTestBase .WithProperty("CustomProperty2", "CustomValue2") .RequireAuthenticated() //For for testing, not so meaningful .RequireGlobalFeatures("GlobalFeature1", "GlobalFeature2") - .RequireFeatures("Feature1", "Feature2") + .RequireFeatures(requiresAll: true, batchCheck: false, "Feature1", "Feature2") .RequirePermissions(requiresAll: false, batchCheck: false,"Permission2", "Permission3"); // Act @@ -96,7 +96,7 @@ public class PermissionDefinitionSerializer_Tests : PermissionTestBase .WithProperty("CustomProperty2", "CustomValue2") .RequireAuthenticated() //For for testing, not so meaningful .RequireGlobalFeatures("GlobalFeature1", "GlobalFeature2") - .RequireFeatures("Feature1", "Feature2") + .RequireFeatures(requiresAll: true, batchCheck: false, "Feature1", "Feature2") .RequirePermissions(requiresAll: false, batchCheck: false,"Permission2", "Permission3"); // Act