mirror of https://github.com/abpframework/abp.git
committed by
GitHub
18 changed files with 626 additions and 35 deletions
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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>>(); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue