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