Browse Source

Merge pull request #25298 from abpframework/auto-merge/rel-10-3/4511

Merge branch dev with rel-10.3
pull/25306/head
Volosoft Agent 1 month ago
committed by GitHub
parent
commit
359df3bf6c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/AbpAspNetCoreMvcUiThemeSharedModule.cs
  2. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Toolbars/ToolbarManager.cs
  3. 1
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj
  4. 28
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs
  5. 51
      framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs
  6. 15
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerBase.cs
  7. 27
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureSimpleStateCheckerExtensions.cs
  8. 6
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs
  9. 5
      framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs
  10. 93
      framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs
  11. 23
      framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateCheckerModel.cs
  12. 1
      framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj
  13. 3
      framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/AbpUiNavigationModule.cs
  14. 2
      framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs
  15. 111
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_BatchSingleScope_Tests.cs
  16. 179
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_ScopeIsolation_Tests.cs
  17. 106
      framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs
  18. 4
      modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs

4
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
{

2
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<ToolbarItem>.Use(new RequirePermissionsSimpleBatchStateChecker<ToolbarItem>()))
using (RequireFeaturesSimpleBatchStateChecker<ToolbarItem>.Use(new RequireFeaturesSimpleBatchStateChecker<ToolbarItem>()))
{
var context = new ToolbarConfigurationContext(ThemeManager.CurrentTheme, toolbar, scope.ServiceProvider);

1
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj

@ -30,6 +30,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.UI.Bootstrap\Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.csproj" />
<ProjectReference Include="..\Volo.Abp.Features\Volo.Abp.Features.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.UI.Packages\Volo.Abp.AspNetCore.Mvc.UI.Packages.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.UI.Widgets\Volo.Abp.AspNetCore.Mvc.UI.Widgets.csproj" />
</ItemGroup>

28
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs

@ -47,13 +47,33 @@ public class RequirePermissionsSimpleBatchStateChecker<TState> : SimpleBatchStat
var result = new SimpleStateCheckerResult<TState>(context.States);
var permissions = _models.Where(x => context.States.Any(s => s.Equals(x.State))).SelectMany(x => x.Permissions).Distinct().ToArray();
var grantResult = await permissionChecker.IsGrantedAsync(permissions);
var stateSet = new HashSet<TState>(context.States);
var modelLookup = new Dictionary<TState, RequirePermissionsSimpleBatchStateCheckerModel<TState>>();
var allPermissions = new HashSet<string>();
foreach (var model in _models)
{
if (!stateSet.Contains(model.State))
{
continue;
}
if (!modelLookup.ContainsKey(model.State))
{
modelLookup[model.State] = model;
}
foreach (var permission in model.Permissions)
{
allPermissions.Add(permission);
}
}
var grantResult = await permissionChecker.IsGrantedAsync(allPermissions.ToArray());
foreach (var state in context.States)
{
var model = _models.FirstOrDefault(x => x.State.Equals(state));
if (model != null)
if (modelLookup.TryGetValue(state, out var model))
{
if (model.RequiresAll)
{

51
framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -57,7 +57,7 @@ public class SimpleStateCheckerManager<TState> : ISimpleStateCheckerManager<TSta
foreach (ISimpleBatchStateChecker<TState> globalStateChecker in Options.GlobalStateCheckers
.Where(x => typeof(ISimpleBatchStateChecker<TState>).IsAssignableFrom(x))
.Select(x => ServiceProvider.GetRequiredService(x)))
.Select(x => scope.ServiceProvider.GetRequiredService(x)))
{
var context = new SimpleBatchStateCheckerContext<TState>(
scope.ServiceProvider.GetRequiredService<ICachedServiceProvider>(),
@ -69,11 +69,15 @@ public class SimpleStateCheckerManager<TState> : ISimpleStateCheckerManager<TSta
}
}
var hasNonBatchGlobalCheckers = Options.GlobalStateCheckers
.Any(x => !typeof(ISimpleBatchStateChecker<TState>).IsAssignableFrom(x));
foreach (var state in states)
{
if (result[state])
if (result[state] &&
(hasNonBatchGlobalCheckers || state.StateCheckers.Any(x => x is not ISimpleBatchStateChecker<TState>)))
{
result[state] = await InternalIsEnabledAsync(state, false);
result[state] = await EvaluateCheckersAsync(state, false, scope);
}
}
@ -96,27 +100,36 @@ public class SimpleStateCheckerManager<TState> : ISimpleStateCheckerManager<TSta
using (var scope = ServiceProvider.CreateScope())
{
var context = new SimpleStateCheckerContext<TState>(scope.ServiceProvider.GetRequiredService<ICachedServiceProvider>(), state);
return await EvaluateCheckersAsync(state, useBatchChecker, scope);
}
}
protected virtual async Task<bool> EvaluateCheckersAsync(TState state, bool useBatchChecker, IServiceScope scope)
{
var context = new SimpleStateCheckerContext<TState>(
!useBatchChecker
? scope.ServiceProvider.GetRequiredService<ITransientCachedServiceProvider>()
: scope.ServiceProvider.GetRequiredService<ICachedServiceProvider>(),
state);
foreach (var provider in state.StateCheckers.WhereIf(!useBatchChecker, x => x is not ISimpleBatchStateChecker<TState>))
foreach (var provider in state.StateCheckers.WhereIf(!useBatchChecker, x => x is not ISimpleBatchStateChecker<TState>))
{
if (!await provider.IsEnabledAsync(context))
{
if (!await provider.IsEnabledAsync(context))
{
return false;
}
return false;
}
}
foreach (ISimpleStateChecker<TState> provider in Options.GlobalStateCheckers
.WhereIf(!useBatchChecker, x => !typeof(ISimpleBatchStateChecker<TState>).IsAssignableFrom(x))
.Select(x => ServiceProvider.GetRequiredService(x)))
foreach (ISimpleStateChecker<TState> provider in Options.GlobalStateCheckers
.WhereIf(!useBatchChecker, x => !typeof(ISimpleBatchStateChecker<TState>).IsAssignableFrom(x))
.Select(x => scope.ServiceProvider.GetRequiredService(x)))
{
if (!await provider.IsEnabledAsync(context))
{
if (!await provider.IsEnabledAsync(context))
{
return false;
}
return false;
}
return true;
}
return true;
}
}

15
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<Dictionary<string, bool>> IsEnabledAsync(string[] names)
{
var result = new Dictionary<string, bool>();
foreach (var name in names)
{
result[name] = await IsEnabledAsync(name);
}
return result;
}
}

27
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<TState>
{
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<TState>
{
state.RequireFeatures(requiresAll: requiresAll, batchCheck: true, features);
return state;
}
public static TState RequireFeatures<TState>(
[NotNull] this TState state,
bool requiresAll,
bool batchCheck,
params string[] features)
where TState : IHasSimpleStateCheckers<TState>
{
Check.NotNull(state, nameof(state));
Check.NotNullOrEmpty(features, nameof(features));
state.StateCheckers.Add(new RequireFeaturesSimpleStateChecker<TState>(requiresAll, features));
if (batchCheck)
{
RequireFeaturesSimpleBatchStateChecker<TState>.Current.AddCheckModels(
new RequireFeaturesSimpleBatchStateCheckerModel<TState>(state, features, requiresAll));
state.StateCheckers.Add(RequireFeaturesSimpleBatchStateChecker<TState>.Current);
}
else
{
state.StateCheckers.Add(new RequireFeaturesSimpleStateChecker<TState>(requiresAll, features));
}
return state;
}
}

6
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<TState>(ISimpleStateChecker<TState> checker)
where TState : IHasSimpleStateCheckers<TState>
{
@ -53,4 +53,4 @@ public class FeaturesSimpleStateCheckerSerializerContributor :
nameArray.Select(x => x!.ToString()).ToArray()
);
}
}
}

5
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<string?> GetOrNullAsync([NotNull] string name);
Task<bool> IsEnabledAsync(string name);
Task<Dictionary<string, bool>> IsEnabledAsync([NotNull] string[] names);
}

93
framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs

@ -0,0 +1,93 @@
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<TState> : SimpleBatchStateCheckerBase<TState>
where TState : IHasSimpleStateCheckers<TState>
{
public static RequireFeaturesSimpleBatchStateChecker<TState> Current => _current.Value!;
private static readonly AsyncLocal<RequireFeaturesSimpleBatchStateChecker<TState>> _current = new();
private readonly List<RequireFeaturesSimpleBatchStateCheckerModel<TState>> _models;
static RequireFeaturesSimpleBatchStateChecker()
{
_current.Value = new RequireFeaturesSimpleBatchStateChecker<TState>();
}
public RequireFeaturesSimpleBatchStateChecker()
{
_models = new List<RequireFeaturesSimpleBatchStateCheckerModel<TState>>();
}
public RequireFeaturesSimpleBatchStateChecker<TState> AddCheckModels(
params RequireFeaturesSimpleBatchStateCheckerModel<TState>[] models)
{
Check.NotNullOrEmpty(models, nameof(models));
_models.AddRange(models);
return this;
}
public static IDisposable Use(RequireFeaturesSimpleBatchStateChecker<TState> checker)
{
var previousValue = Current;
_current.Value = checker;
return new DisposeAction(() => _current.Value = previousValue);
}
public override async Task<SimpleStateCheckerResult<TState>> IsEnabledAsync(
SimpleBatchStateCheckerContext<TState> context)
{
var featureChecker = context.ServiceProvider.GetRequiredService<IFeatureChecker>();
var result = new SimpleStateCheckerResult<TState>(context.States);
var stateSet = new HashSet<TState>(context.States);
var modelLookup = new Dictionary<TState, RequireFeaturesSimpleBatchStateCheckerModel<TState>>();
var allFeatures = new HashSet<string>();
foreach (var model in _models)
{
if (!stateSet.Contains(model.State))
{
continue;
}
if (!modelLookup.ContainsKey(model.State))
{
modelLookup[model.State] = model;
}
foreach (var featureName in model.FeatureNames)
{
allFeatures.Add(featureName);
}
}
var featureValues = await featureChecker.IsEnabledAsync(allFeatures.ToArray());
foreach (var state in context.States)
{
if (modelLookup.TryGetValue(state, out var model))
{
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;
}
}

23
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<TState>
where TState : IHasSimpleStateCheckers<TState>
{
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;
}
}

1
framework/src/Volo.Abp.UI.Navigation/Volo.Abp.UI.Navigation.csproj

@ -23,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Authorization\Volo.Abp.Authorization.csproj" />
<ProjectReference Include="..\Volo.Abp.Features\Volo.Abp.Features.csproj" />
<ProjectReference Include="..\Volo.Abp.UI\Volo.Abp.UI.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy\Volo.Abp.MultiTenancy.csproj" />
</ItemGroup>

3
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)

2
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<ApplicationMenuItem>.Use(new RequirePermissionsSimpleBatchStateChecker<ApplicationMenuItem>()))
using (RequireFeaturesSimpleBatchStateChecker<ApplicationMenuItem>.Use(new RequireFeaturesSimpleBatchStateChecker<ApplicationMenuItem>()))
{
var context = new MenuConfigurationContext(menu, scope.ServiceProvider);

111
framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_BatchSingleScope_Tests.cs

@ -0,0 +1,111 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.SimpleStateChecking;
/// <summary>
/// Tests that batch IsEnabledAsync evaluates non-batch state checkers correctly
/// when reusing a single DI scope (instead of creating N scopes via InternalIsEnabledAsync).
/// </summary>
public class SimpleStateChecker_BatchSingleScope_Tests : SimpleStateCheckerTestBase
{
[Fact]
public async Task Batch_Should_Evaluate_NonBatch_Checkers_Correctly()
{
var enabled = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
enabled.AddSimpleStateChecker(new MySimpleStateChecker());
var disabled = new MyStateEntity
{
CreationTime = DateTime.Parse("2001-01-01", CultureInfo.InvariantCulture)
};
disabled.AddSimpleStateChecker(new MySimpleStateChecker());
var result = await SimpleStateCheckerManager.IsEnabledAsync(new[] { enabled, disabled });
result[enabled].ShouldBeTrue();
result[disabled].ShouldBeFalse();
enabled.CheckCount.ShouldBe(1);
disabled.CheckCount.ShouldBe(1);
}
[Fact]
public async Task Batch_Should_Skip_NonBatch_Check_When_BatchChecker_Already_Disabled()
{
// Entity disabled by batch checker should not have non-batch checker invoked
var entity = new MyStateEntity
{
CreationTime = DateTime.Parse("2001-01-01", CultureInfo.InvariantCulture) // fails batch checker
};
entity.AddSimpleStateChecker(new MySimpleBatchStateChecker());
entity.AddSimpleStateChecker(new MySimpleStateChecker());
var result = await SimpleStateCheckerManager.IsEnabledAsync(new[] { entity });
result[entity].ShouldBeFalse();
entity.MultipleCheckCount.ShouldBe(1); // batch checker was called
entity.CheckCount.ShouldBe(0); // non-batch checker was NOT called (skipped because batch disabled it)
}
[Fact]
public async Task Batch_Should_Handle_Mix_Of_Entities_With_And_Without_Checkers()
{
var noChecker = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
var withChecker = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
withChecker.AddSimpleStateChecker(new MySimpleStateChecker());
var failingChecker = new MyStateEntity
{
CreationTime = DateTime.Parse("2001-01-01", CultureInfo.InvariantCulture)
};
failingChecker.AddSimpleStateChecker(new MySimpleStateChecker());
var result = await SimpleStateCheckerManager.IsEnabledAsync(
new[] { noChecker, withChecker, failingChecker });
result[noChecker].ShouldBeTrue();
result[withChecker].ShouldBeTrue();
result[failingChecker].ShouldBeFalse();
noChecker.CheckCount.ShouldBe(0);
withChecker.CheckCount.ShouldBe(1);
failingChecker.CheckCount.ShouldBe(1);
}
[Fact]
public async Task Batch_Should_Handle_Large_Number_Of_Entities()
{
var entities = new MyStateEntity[1000];
for (int i = 0; i < 1000; i++)
{
entities[i] = new MyStateEntity
{
CreationTime = i % 2 == 0
? DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
: DateTime.Parse("2001-01-01", CultureInfo.InvariantCulture)
};
entities[i].AddSimpleStateChecker(new MySimpleStateChecker());
}
var result = await SimpleStateCheckerManager.IsEnabledAsync(entities);
for (int i = 0; i < 1000; i++)
{
result[entities[i]].ShouldBe(i % 2 == 0);
entities[i].CheckCount.ShouldBe(1);
}
}
}

179
framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_ScopeIsolation_Tests.cs

@ -0,0 +1,179 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;
namespace Volo.Abp.SimpleStateChecking;
/// <summary>
/// Probe-based tests that verify:
/// 1. Batch path shares a single DI scope but isolates CachedServiceProvider per state.
/// 2. Single-state path uses separate scopes (original behavior).
/// 3. Nested single-state calls from within batch checkers get their own scope.
/// </summary>
public class SimpleStateChecker_ScopeIsolation_Tests : SimpleStateCheckerTestBase
{
protected override void AfterAddApplication(IServiceCollection services)
{
services.AddScoped<ScopeIdProbe>();
services.AddTransient<TransientIdProbe>();
base.AfterAddApplication(services);
}
[Fact]
public async Task Batch_Should_Share_Scope_But_Isolate_CachedProvider_Across_States()
{
var observations = new ConcurrentDictionary<MyStateEntity, Observation>();
var checker = new ScopeProbeStateChecker(observations);
var stateA = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
stateA.AddSimpleStateChecker(checker);
var stateB = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
stateB.AddSimpleStateChecker(checker);
var stateC = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
stateC.AddSimpleStateChecker(checker);
await SimpleStateCheckerManager.IsEnabledAsync(new[] { stateA, stateB, stateC });
observations.Count.ShouldBe(3);
// All states in the batch should see the same scoped service (shared DI scope)
observations[stateA].ScopeId.ShouldBe(observations[stateB].ScopeId);
observations[stateB].ScopeId.ShouldBe(observations[stateC].ScopeId);
// Each state should get its own transient instance (isolated cached provider)
observations[stateA].TransientId.ShouldNotBe(observations[stateB].TransientId);
observations[stateB].TransientId.ShouldNotBe(observations[stateC].TransientId);
}
[Fact]
public async Task Single_State_Calls_Should_Use_Separate_Scopes()
{
var observations = new ConcurrentDictionary<MyStateEntity, Observation>();
var checker = new ScopeProbeStateChecker(observations);
var stateA = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
stateA.AddSimpleStateChecker(checker);
var stateB = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
stateB.AddSimpleStateChecker(checker);
await SimpleStateCheckerManager.IsEnabledAsync(stateA);
await SimpleStateCheckerManager.IsEnabledAsync(stateB);
observations.Count.ShouldBe(2);
// Single-state calls should each get their own scope
observations[stateA].ScopeId.ShouldNotBe(observations[stateB].ScopeId);
}
[Fact]
public async Task Nested_Single_State_Call_From_Batch_Checker_Should_Get_Own_Scope()
{
var observations = new ConcurrentDictionary<MyStateEntity, Observation>();
// This state will be evaluated via a nested single-state call during batch evaluation
var nestedState = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
nestedState.AddSimpleStateChecker(new ScopeProbeStateChecker(observations));
// This checker triggers a nested IsEnabledAsync(state) during batch evaluation
var nestedCallChecker = new NestedSingleCallChecker(
SimpleStateCheckerManager, nestedState, observations);
var batchState = new MyStateEntity
{
CreationTime = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture)
};
batchState.AddSimpleStateChecker(nestedCallChecker);
await SimpleStateCheckerManager.IsEnabledAsync(new[] { batchState });
observations.Count.ShouldBe(2);
// The nested single-state call should get its own scope, not the batch scope
observations[batchState].ScopeId.ShouldNotBe(observations[nestedState].ScopeId);
}
public record Observation(Guid ScopeId, Guid TransientId);
public class ScopeIdProbe
{
public Guid Id { get; } = Guid.NewGuid();
}
public class TransientIdProbe
{
public Guid Id { get; } = Guid.NewGuid();
}
public class ScopeProbeStateChecker : ISimpleStateChecker<MyStateEntity>
{
private readonly ConcurrentDictionary<MyStateEntity, Observation> _observations;
public ScopeProbeStateChecker(
ConcurrentDictionary<MyStateEntity, Observation> observations)
{
_observations = observations;
}
public Task<bool> IsEnabledAsync(SimpleStateCheckerContext<MyStateEntity> context)
{
var scopeProbe = context.ServiceProvider.GetRequiredService<ScopeIdProbe>();
var transientProbe = context.ServiceProvider.GetRequiredService<TransientIdProbe>();
_observations[context.State] = new Observation(scopeProbe.Id, transientProbe.Id);
return Task.FromResult(true);
}
}
public class NestedSingleCallChecker : ISimpleStateChecker<MyStateEntity>
{
private readonly ISimpleStateCheckerManager<MyStateEntity> _manager;
private readonly MyStateEntity _nestedState;
private readonly ConcurrentDictionary<MyStateEntity, Observation> _observations;
public NestedSingleCallChecker(
ISimpleStateCheckerManager<MyStateEntity> manager,
MyStateEntity nestedState,
ConcurrentDictionary<MyStateEntity, Observation> observations)
{
_manager = manager;
_nestedState = nestedState;
_observations = observations;
}
public async Task<bool> IsEnabledAsync(SimpleStateCheckerContext<MyStateEntity> context)
{
var scopeProbe = context.ServiceProvider.GetRequiredService<ScopeIdProbe>();
var transientProbe = context.ServiceProvider.GetRequiredService<TransientIdProbe>();
_observations[context.State] = new Observation(scopeProbe.Id, transientProbe.Id);
// Trigger a nested single-state call during batch evaluation
await _manager.IsEnabledAsync(_nestedState);
return true;
}
}
}

106
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<MyStateEntity> _simpleStateCheckerManager;
private readonly ICurrentTenant _currentTenant;
public RequireFeaturesSimpleBatchStateChecker_Tests()
{
_simpleStateCheckerManager = GetRequiredService<ISimpleStateCheckerManager<MyStateEntity>>();
_currentTenant = GetRequiredService<ICurrentTenant>();
}
[Fact]
public void Switch_Current_Checker_Test()
{
var checker = RequireFeaturesSimpleBatchStateChecker<MyStateEntity2>.Current;
checker.ShouldNotBeNull();
RequireFeaturesSimpleBatchStateChecker<MyStateEntity2> checker2 = null;
using (RequireFeaturesSimpleBatchStateChecker<MyStateEntity2>.Use(new RequireFeaturesSimpleBatchStateChecker<MyStateEntity2>()))
{
checker2 = RequireFeaturesSimpleBatchStateChecker<MyStateEntity2>.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<MyStateEntity>
{
public List<ISimpleStateChecker<MyStateEntity>> StateCheckers { get; }
public MyStateEntity()
{
StateCheckers = new List<ISimpleStateChecker<MyStateEntity>>();
}
}
class MyStateEntity2 : IHasSimpleStateCheckers<MyStateEntity2>
{
public List<ISimpleStateChecker<MyStateEntity2>> StateCheckers { get; }
public MyStateEntity2()
{
StateCheckers = new List<ISimpleStateChecker<MyStateEntity2>>();
}
}
}

4
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

Loading…
Cancel
Save